@syncular/typegen 0.4.1 → 0.5.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 +38 -9
- package/dist/emit-queries.js +63 -5
- package/dist/emit.js +32 -0
- package/dist/generate.d.ts +4 -0
- package/dist/generate.js +18 -6
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/manifest.d.ts +3 -0
- package/dist/manifest.js +8 -2
- package/dist/query-ir.js +24 -1
- package/dist/query.d.ts +30 -0
- package/dist/query.js +139 -8
- package/dist/syql-ast.d.ts +95 -0
- package/dist/syql-ast.js +138 -0
- package/dist/syql-lexer.d.ts +40 -0
- package/dist/syql-lexer.js +367 -0
- package/dist/syql-modules.d.ts +21 -0
- package/dist/syql-modules.js +129 -0
- package/dist/syql-parser.d.ts +132 -0
- package/dist/syql-parser.js +604 -0
- package/dist/syql-template-parser.d.ts +58 -0
- package/dist/syql-template-parser.js +350 -0
- package/dist/syql.d.ts +19 -0
- package/dist/syql.js +196 -0
- package/package.json +3 -3
- package/src/emit-queries.ts +86 -5
- package/src/emit.ts +38 -0
- package/src/generate.ts +32 -5
- package/src/index.ts +5 -0
- package/src/manifest.ts +11 -2
- package/src/query-ir.ts +24 -1
- package/src/query.ts +207 -8
- package/src/syql-ast.ts +277 -0
- package/src/syql-lexer.ts +514 -0
- package/src/syql-modules.ts +217 -0
- package/src/syql-parser.ts +1021 -0
- package/src/syql-template-parser.ts +582 -0
- package/src/syql.ts +300 -1
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
/** Revision-1 SYQL container parser (`docs/SYQL.md` §3). */
|
|
2
|
+
import {
|
|
3
|
+
isSyqlTrivia,
|
|
4
|
+
lexSyqlSource,
|
|
5
|
+
SyqlFrontendError,
|
|
6
|
+
type SyqlSourceSpan,
|
|
7
|
+
type SyqlToken,
|
|
8
|
+
} from './syql-lexer';
|
|
9
|
+
import {
|
|
10
|
+
parseSyqlEmbeddedTemplate,
|
|
11
|
+
type SyqlEmbeddedTemplate,
|
|
12
|
+
type SyqlTemplateMode,
|
|
13
|
+
} from './syql-template-parser';
|
|
14
|
+
|
|
15
|
+
export type SyqlBaseType =
|
|
16
|
+
| 'string'
|
|
17
|
+
| 'integer'
|
|
18
|
+
| 'float'
|
|
19
|
+
| 'boolean'
|
|
20
|
+
| 'json'
|
|
21
|
+
| 'bytes'
|
|
22
|
+
| 'blob_ref'
|
|
23
|
+
| 'crdt';
|
|
24
|
+
|
|
25
|
+
export interface SyqlValueType {
|
|
26
|
+
readonly base: SyqlBaseType;
|
|
27
|
+
readonly nullable: boolean;
|
|
28
|
+
readonly span: SyqlSourceSpan;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SyqlValueParameter {
|
|
32
|
+
readonly kind: 'value';
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly optional: boolean;
|
|
35
|
+
readonly type?: SyqlValueType;
|
|
36
|
+
readonly span: SyqlSourceSpan;
|
|
37
|
+
readonly nameSpan: SyqlSourceSpan;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SyqlSwitchParameter {
|
|
41
|
+
readonly kind: 'switch';
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly optional: true;
|
|
44
|
+
readonly span: SyqlSourceSpan;
|
|
45
|
+
readonly nameSpan: SyqlSourceSpan;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SyqlGroupMember {
|
|
49
|
+
readonly name: string;
|
|
50
|
+
readonly type?: SyqlValueType;
|
|
51
|
+
readonly span: SyqlSourceSpan;
|
|
52
|
+
readonly nameSpan: SyqlSourceSpan;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SyqlGroupParameter {
|
|
56
|
+
readonly kind: 'group';
|
|
57
|
+
readonly name: string;
|
|
58
|
+
readonly optional: true;
|
|
59
|
+
readonly members: readonly SyqlGroupMember[];
|
|
60
|
+
readonly span: SyqlSourceSpan;
|
|
61
|
+
readonly nameSpan: SyqlSourceSpan;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type SyqlQueryParameter =
|
|
65
|
+
| SyqlValueParameter
|
|
66
|
+
| SyqlSwitchParameter
|
|
67
|
+
| SyqlGroupParameter;
|
|
68
|
+
|
|
69
|
+
export interface SyqlPredicateParameter {
|
|
70
|
+
readonly name: string;
|
|
71
|
+
readonly type?: SyqlValueType;
|
|
72
|
+
readonly span: SyqlSourceSpan;
|
|
73
|
+
readonly nameSpan: SyqlSourceSpan;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A lossless SQL token template between a pair of container braces. */
|
|
77
|
+
export interface SyqlTemplate {
|
|
78
|
+
readonly text: string;
|
|
79
|
+
readonly tokens: readonly SyqlToken[];
|
|
80
|
+
/** Structurally parsed SYQL nodes embedded in the otherwise-lossless SQL. */
|
|
81
|
+
readonly tree: SyqlEmbeddedTemplate;
|
|
82
|
+
/** Span of the inner text, excluding braces. */
|
|
83
|
+
readonly span: SyqlSourceSpan;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface SyqlImportItem {
|
|
87
|
+
readonly imported: string;
|
|
88
|
+
readonly local: string;
|
|
89
|
+
readonly span: SyqlSourceSpan;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface SyqlImportDeclaration {
|
|
93
|
+
readonly kind: 'import';
|
|
94
|
+
readonly items: readonly SyqlImportItem[];
|
|
95
|
+
/** Decoded JSON path. */
|
|
96
|
+
readonly path: string;
|
|
97
|
+
readonly span: SyqlSourceSpan;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface SyqlPredicateDeclaration {
|
|
101
|
+
readonly kind: 'predicate';
|
|
102
|
+
readonly name: string;
|
|
103
|
+
readonly parameters: readonly SyqlPredicateParameter[];
|
|
104
|
+
readonly body: SyqlTemplate;
|
|
105
|
+
readonly span: SyqlSourceSpan;
|
|
106
|
+
readonly nameSpan: SyqlSourceSpan;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface SyqlSqlSection {
|
|
110
|
+
readonly kind: 'sql';
|
|
111
|
+
readonly body: SyqlTemplate;
|
|
112
|
+
readonly span: SyqlSourceSpan;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface SyqlSortProfile {
|
|
116
|
+
readonly name: string;
|
|
117
|
+
readonly order: SyqlTemplate;
|
|
118
|
+
readonly span: SyqlSourceSpan;
|
|
119
|
+
readonly nameSpan: SyqlSourceSpan;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface SyqlSortSection {
|
|
123
|
+
readonly kind: 'sort';
|
|
124
|
+
readonly control: string;
|
|
125
|
+
readonly defaultProfile: string;
|
|
126
|
+
readonly profiles: readonly SyqlSortProfile[];
|
|
127
|
+
readonly span: SyqlSourceSpan;
|
|
128
|
+
readonly controlSpan: SyqlSourceSpan;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface SyqlPageDeclaration {
|
|
132
|
+
readonly kind: 'page';
|
|
133
|
+
readonly control: string;
|
|
134
|
+
readonly defaultSize: number;
|
|
135
|
+
readonly maxSize: number;
|
|
136
|
+
readonly span: SyqlSourceSpan;
|
|
137
|
+
readonly controlSpan: SyqlSourceSpan;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface SyqlIdentityDeclaration {
|
|
141
|
+
readonly kind: 'identity';
|
|
142
|
+
readonly fields: readonly string[];
|
|
143
|
+
readonly span: SyqlSourceSpan;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface SyqlQueryDeclaration {
|
|
147
|
+
readonly kind: 'query';
|
|
148
|
+
readonly name: string;
|
|
149
|
+
readonly parameters: readonly SyqlQueryParameter[];
|
|
150
|
+
readonly sql: SyqlSqlSection;
|
|
151
|
+
readonly sort?: SyqlSortSection;
|
|
152
|
+
readonly page?: SyqlPageDeclaration;
|
|
153
|
+
readonly identity?: SyqlIdentityDeclaration;
|
|
154
|
+
readonly span: SyqlSourceSpan;
|
|
155
|
+
readonly nameSpan: SyqlSourceSpan;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type SyqlDeclaration = SyqlPredicateDeclaration | SyqlQueryDeclaration;
|
|
159
|
+
|
|
160
|
+
export interface SyqlSyntaxFile {
|
|
161
|
+
readonly kind: 'file';
|
|
162
|
+
readonly file: string;
|
|
163
|
+
readonly source: string;
|
|
164
|
+
readonly tokens: readonly SyqlToken[];
|
|
165
|
+
readonly imports: readonly SyqlImportDeclaration[];
|
|
166
|
+
readonly declarations: readonly SyqlDeclaration[];
|
|
167
|
+
readonly predicates: readonly SyqlPredicateDeclaration[];
|
|
168
|
+
readonly queries: readonly SyqlQueryDeclaration[];
|
|
169
|
+
readonly span: SyqlSourceSpan;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export type SyqlParseErrorCode =
|
|
173
|
+
| 'SYQL2001_EXPECTED_TOKEN'
|
|
174
|
+
| 'SYQL2002_INVALID_NAME'
|
|
175
|
+
| 'SYQL2003_RESERVED_NAME'
|
|
176
|
+
| 'SYQL2004_DUPLICATE_NAME'
|
|
177
|
+
| 'SYQL2005_INVALID_IMPORT'
|
|
178
|
+
| 'SYQL2006_EMPTY_TEMPLATE'
|
|
179
|
+
| 'SYQL2007_FORBIDDEN_SEMICOLON'
|
|
180
|
+
| 'SYQL2008_INVALID_MEMBER'
|
|
181
|
+
| 'SYQL2009_INVALID_INTEGER'
|
|
182
|
+
| 'SYQL2010_INVALID_PAGE_RANGE'
|
|
183
|
+
| 'SYQL2011_INVALID_PARAMETER';
|
|
184
|
+
|
|
185
|
+
const CAMEL_IDENT_RE = /^[a-z][A-Za-z0-9]*$/;
|
|
186
|
+
const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
187
|
+
const INTEGER_LITERAL_RE = /^(?:0|[1-9][0-9]*)$/;
|
|
188
|
+
const BASE_TYPES = new Set<SyqlBaseType>([
|
|
189
|
+
'string',
|
|
190
|
+
'integer',
|
|
191
|
+
'float',
|
|
192
|
+
'boolean',
|
|
193
|
+
'json',
|
|
194
|
+
'bytes',
|
|
195
|
+
'blob_ref',
|
|
196
|
+
'crdt',
|
|
197
|
+
]);
|
|
198
|
+
const RESERVED_NAMES = new Set([
|
|
199
|
+
'as',
|
|
200
|
+
'blob_ref',
|
|
201
|
+
'boolean',
|
|
202
|
+
'by',
|
|
203
|
+
'bytes',
|
|
204
|
+
'cover',
|
|
205
|
+
'crdt',
|
|
206
|
+
'default',
|
|
207
|
+
'float',
|
|
208
|
+
'from',
|
|
209
|
+
'identity',
|
|
210
|
+
'import',
|
|
211
|
+
'in',
|
|
212
|
+
'integer',
|
|
213
|
+
'json',
|
|
214
|
+
'max',
|
|
215
|
+
'null',
|
|
216
|
+
'page',
|
|
217
|
+
'predicate',
|
|
218
|
+
'query',
|
|
219
|
+
'scope',
|
|
220
|
+
'sort',
|
|
221
|
+
'sql',
|
|
222
|
+
'string',
|
|
223
|
+
'switch',
|
|
224
|
+
'when',
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
interface ParsedBlock {
|
|
228
|
+
readonly template: SyqlTemplate;
|
|
229
|
+
readonly open: SyqlToken;
|
|
230
|
+
readonly close: SyqlToken;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
interface ParsedQueryParameters {
|
|
234
|
+
readonly parameters: readonly SyqlQueryParameter[];
|
|
235
|
+
readonly publicNames: Set<string>;
|
|
236
|
+
readonly bindNames: Set<string>;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function spanBetween(start: SyqlToken, end: SyqlToken): SyqlSourceSpan {
|
|
240
|
+
return {
|
|
241
|
+
file: start.span.file,
|
|
242
|
+
start: start.span.start,
|
|
243
|
+
end: end.span.end,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function spanFromPositions(
|
|
248
|
+
file: string,
|
|
249
|
+
start: SyqlSourceSpan['start'],
|
|
250
|
+
end: SyqlSourceSpan['end'],
|
|
251
|
+
): SyqlSourceSpan {
|
|
252
|
+
return { file, start, end };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
class Parser {
|
|
256
|
+
readonly #file: string;
|
|
257
|
+
readonly #source: string;
|
|
258
|
+
readonly #tokens: readonly SyqlToken[];
|
|
259
|
+
#index = 0;
|
|
260
|
+
|
|
261
|
+
constructor(file: string, source: string) {
|
|
262
|
+
this.#file = file;
|
|
263
|
+
this.#source = source;
|
|
264
|
+
this.#tokens = lexSyqlSource(file, source);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
parse(): SyqlSyntaxFile {
|
|
268
|
+
const imports: SyqlImportDeclaration[] = [];
|
|
269
|
+
const declarations: SyqlDeclaration[] = [];
|
|
270
|
+
const predicates: SyqlPredicateDeclaration[] = [];
|
|
271
|
+
const queries: SyqlQueryDeclaration[] = [];
|
|
272
|
+
const fileNames = new Map<string, SyqlSourceSpan>();
|
|
273
|
+
let sawDeclaration = false;
|
|
274
|
+
|
|
275
|
+
while (this.#peek().kind !== 'eof') {
|
|
276
|
+
const keyword = this.#peek();
|
|
277
|
+
if (keyword.text === 'import') {
|
|
278
|
+
if (sawDeclaration) {
|
|
279
|
+
this.#fail(
|
|
280
|
+
'SYQL2008_INVALID_MEMBER',
|
|
281
|
+
keyword,
|
|
282
|
+
'imports must precede all predicate and query declarations',
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const declaration = this.#parseImport();
|
|
286
|
+
for (const item of declaration.items) {
|
|
287
|
+
this.#claimName(fileNames, item.local, item.span, 'imported name');
|
|
288
|
+
}
|
|
289
|
+
imports.push(declaration);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
sawDeclaration = true;
|
|
294
|
+
if (keyword.text === 'predicate') {
|
|
295
|
+
const declaration = this.#parsePredicate();
|
|
296
|
+
this.#claimName(
|
|
297
|
+
fileNames,
|
|
298
|
+
declaration.name,
|
|
299
|
+
declaration.nameSpan,
|
|
300
|
+
'declaration',
|
|
301
|
+
);
|
|
302
|
+
declarations.push(declaration);
|
|
303
|
+
predicates.push(declaration);
|
|
304
|
+
} else if (keyword.text === 'query') {
|
|
305
|
+
const declaration = this.#parseQuery();
|
|
306
|
+
this.#claimName(
|
|
307
|
+
fileNames,
|
|
308
|
+
declaration.name,
|
|
309
|
+
declaration.nameSpan,
|
|
310
|
+
'declaration',
|
|
311
|
+
);
|
|
312
|
+
declarations.push(declaration);
|
|
313
|
+
queries.push(declaration);
|
|
314
|
+
} else {
|
|
315
|
+
this.#fail(
|
|
316
|
+
'SYQL2008_INVALID_MEMBER',
|
|
317
|
+
keyword,
|
|
318
|
+
`expected import, predicate, or query; found ${JSON.stringify(keyword.text)}`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const first = this.#tokens[0] as SyqlToken;
|
|
324
|
+
const eof = this.#tokens[this.#tokens.length - 1] as SyqlToken;
|
|
325
|
+
return {
|
|
326
|
+
kind: 'file',
|
|
327
|
+
file: this.#file,
|
|
328
|
+
source: this.#source,
|
|
329
|
+
tokens: this.#tokens,
|
|
330
|
+
imports,
|
|
331
|
+
declarations,
|
|
332
|
+
predicates,
|
|
333
|
+
queries,
|
|
334
|
+
span: spanFromPositions(this.#file, first.span.start, eof.span.end),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
#parseImport(): SyqlImportDeclaration {
|
|
339
|
+
const start = this.#expectText('import');
|
|
340
|
+
this.#expectText('{');
|
|
341
|
+
const items: SyqlImportItem[] = [];
|
|
342
|
+
const localNames = new Map<string, SyqlSourceSpan>();
|
|
343
|
+
const importedNames = new Map<string, SyqlSourceSpan>();
|
|
344
|
+
|
|
345
|
+
if (this.#peek().text === '}') {
|
|
346
|
+
this.#fail(
|
|
347
|
+
'SYQL2005_INVALID_IMPORT',
|
|
348
|
+
this.#peek(),
|
|
349
|
+
'an import list must contain at least one predicate name',
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
for (;;) {
|
|
354
|
+
const importedToken = this.#parseCamelName('imported predicate name');
|
|
355
|
+
this.#claimName(
|
|
356
|
+
importedNames,
|
|
357
|
+
importedToken.text,
|
|
358
|
+
importedToken.span,
|
|
359
|
+
'imported predicate',
|
|
360
|
+
);
|
|
361
|
+
let localToken = importedToken;
|
|
362
|
+
if (this.#peek().text === 'as') {
|
|
363
|
+
this.#take();
|
|
364
|
+
localToken = this.#parseCamelName('import alias');
|
|
365
|
+
}
|
|
366
|
+
this.#claimName(
|
|
367
|
+
localNames,
|
|
368
|
+
localToken.text,
|
|
369
|
+
localToken.span,
|
|
370
|
+
'import alias',
|
|
371
|
+
);
|
|
372
|
+
items.push({
|
|
373
|
+
imported: importedToken.text,
|
|
374
|
+
local: localToken.text,
|
|
375
|
+
span: spanBetween(importedToken, localToken),
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
if (this.#peek().text !== ',') break;
|
|
379
|
+
this.#take();
|
|
380
|
+
if (this.#peek().text === '}') break;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
this.#expectText('}');
|
|
384
|
+
this.#expectText('from');
|
|
385
|
+
const pathToken = this.#take();
|
|
386
|
+
if (pathToken.kind !== 'import-path') {
|
|
387
|
+
this.#fail(
|
|
388
|
+
'SYQL2005_INVALID_IMPORT',
|
|
389
|
+
pathToken,
|
|
390
|
+
'expected a JSON double-quoted relative .syql import path',
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
let path: unknown;
|
|
394
|
+
try {
|
|
395
|
+
path = JSON.parse(pathToken.text);
|
|
396
|
+
} catch {
|
|
397
|
+
this.#fail(
|
|
398
|
+
'SYQL2005_INVALID_IMPORT',
|
|
399
|
+
pathToken,
|
|
400
|
+
'invalid JSON import path',
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
if (
|
|
404
|
+
typeof path !== 'string' ||
|
|
405
|
+
(!path.startsWith('./') && !path.startsWith('../')) ||
|
|
406
|
+
!path.endsWith('.syql') ||
|
|
407
|
+
path.includes('\0') ||
|
|
408
|
+
path.includes('\\')
|
|
409
|
+
) {
|
|
410
|
+
this.#fail(
|
|
411
|
+
'SYQL2005_INVALID_IMPORT',
|
|
412
|
+
pathToken,
|
|
413
|
+
'import path must be a slash-separated relative path ending in .syql',
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
const end = this.#expectText(';');
|
|
417
|
+
return { kind: 'import', items, path, span: spanBetween(start, end) };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
#parsePredicate(): SyqlPredicateDeclaration {
|
|
421
|
+
const start = this.#expectText('predicate');
|
|
422
|
+
const name = this.#parseCamelName('predicate name');
|
|
423
|
+
this.#expectText('(');
|
|
424
|
+
const parameters: SyqlPredicateParameter[] = [];
|
|
425
|
+
const names = new Map<string, SyqlSourceSpan>();
|
|
426
|
+
if (this.#peek().text !== ')') {
|
|
427
|
+
for (;;) {
|
|
428
|
+
const paramStart = this.#parseCamelName('predicate parameter');
|
|
429
|
+
const type =
|
|
430
|
+
this.#peek().text === ':' ? this.#parseTypeAnnotation() : undefined;
|
|
431
|
+
const span = spanFromPositions(
|
|
432
|
+
this.#file,
|
|
433
|
+
paramStart.span.start,
|
|
434
|
+
(type?.span ?? paramStart.span).end,
|
|
435
|
+
);
|
|
436
|
+
this.#claimName(
|
|
437
|
+
names,
|
|
438
|
+
paramStart.text,
|
|
439
|
+
paramStart.span,
|
|
440
|
+
'predicate parameter',
|
|
441
|
+
);
|
|
442
|
+
parameters.push({
|
|
443
|
+
name: paramStart.text,
|
|
444
|
+
...(type === undefined ? {} : { type }),
|
|
445
|
+
span,
|
|
446
|
+
nameSpan: paramStart.span,
|
|
447
|
+
});
|
|
448
|
+
if (this.#peek().text !== ',') break;
|
|
449
|
+
this.#take();
|
|
450
|
+
if (this.#peek().text === ')') break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
this.#expectText(')');
|
|
454
|
+
const body = this.#parseTemplateBlock('predicate body', 'predicate');
|
|
455
|
+
return {
|
|
456
|
+
kind: 'predicate',
|
|
457
|
+
name: name.text,
|
|
458
|
+
parameters,
|
|
459
|
+
body: body.template,
|
|
460
|
+
span: spanBetween(start, body.close),
|
|
461
|
+
nameSpan: name.span,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
#parseQuery(): SyqlQueryDeclaration {
|
|
466
|
+
const start = this.#expectText('query');
|
|
467
|
+
const name = this.#parseCamelName('query name');
|
|
468
|
+
const parsedParameters = this.#parseQueryParameters();
|
|
469
|
+
const publicNames = parsedParameters.publicNames;
|
|
470
|
+
this.#expectText('{');
|
|
471
|
+
|
|
472
|
+
if (this.#peek().text !== 'sql') {
|
|
473
|
+
this.#fail(
|
|
474
|
+
'SYQL2008_INVALID_MEMBER',
|
|
475
|
+
this.#peek(),
|
|
476
|
+
'a query body must begin with exactly one sql { ... } section',
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
const sql = this.#parseSqlSection();
|
|
480
|
+
|
|
481
|
+
let sort: SyqlSortSection | undefined;
|
|
482
|
+
let page: SyqlPageDeclaration | undefined;
|
|
483
|
+
let identity: SyqlIdentityDeclaration | undefined;
|
|
484
|
+
|
|
485
|
+
if (this.#peek().text === 'sort') {
|
|
486
|
+
sort = this.#parseSortSection(publicNames);
|
|
487
|
+
}
|
|
488
|
+
if (this.#peek().text === 'page') {
|
|
489
|
+
page = this.#parsePageDeclaration(publicNames);
|
|
490
|
+
}
|
|
491
|
+
if (this.#peek().text === 'identity') {
|
|
492
|
+
identity = this.#parseIdentityDeclaration();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (this.#peek().text !== '}') {
|
|
496
|
+
this.#fail(
|
|
497
|
+
'SYQL2008_INVALID_MEMBER',
|
|
498
|
+
this.#peek(),
|
|
499
|
+
`unexpected or out-of-order query member ${JSON.stringify(this.#peek().text)}`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
const close = this.#expectText('}');
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
kind: 'query',
|
|
506
|
+
name: name.text,
|
|
507
|
+
parameters: parsedParameters.parameters,
|
|
508
|
+
sql,
|
|
509
|
+
...(sort === undefined ? {} : { sort }),
|
|
510
|
+
...(page === undefined ? {} : { page }),
|
|
511
|
+
...(identity === undefined ? {} : { identity }),
|
|
512
|
+
span: spanBetween(start, close),
|
|
513
|
+
nameSpan: name.span,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
#parseQueryParameters(): ParsedQueryParameters {
|
|
518
|
+
this.#expectText('(');
|
|
519
|
+
const parameters: SyqlQueryParameter[] = [];
|
|
520
|
+
const publicNames = new Set<string>();
|
|
521
|
+
const bindNames = new Set<string>();
|
|
522
|
+
|
|
523
|
+
if (this.#peek().text !== ')') {
|
|
524
|
+
for (;;) {
|
|
525
|
+
const name = this.#parseCamelName('query parameter');
|
|
526
|
+
const optional = this.#peek().text === '?';
|
|
527
|
+
if (optional) this.#take();
|
|
528
|
+
|
|
529
|
+
if (optional && this.#peek().text === '(') {
|
|
530
|
+
if (publicNames.has(name.text)) {
|
|
531
|
+
this.#duplicate(name, 'public query input');
|
|
532
|
+
}
|
|
533
|
+
publicNames.add(name.text);
|
|
534
|
+
this.#take();
|
|
535
|
+
const members: SyqlGroupMember[] = [];
|
|
536
|
+
for (;;) {
|
|
537
|
+
const memberName = this.#parseCamelName('group member');
|
|
538
|
+
if (memberName.text === name.text) {
|
|
539
|
+
this.#fail(
|
|
540
|
+
'SYQL2011_INVALID_PARAMETER',
|
|
541
|
+
memberName,
|
|
542
|
+
`group ${name.text} cannot contain a member with the same name`,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
if (bindNames.has(memberName.text)) {
|
|
546
|
+
this.#duplicate(memberName, 'query bind');
|
|
547
|
+
}
|
|
548
|
+
bindNames.add(memberName.text);
|
|
549
|
+
const type =
|
|
550
|
+
this.#peek().text === ':'
|
|
551
|
+
? this.#parseTypeAnnotation()
|
|
552
|
+
: undefined;
|
|
553
|
+
members.push({
|
|
554
|
+
name: memberName.text,
|
|
555
|
+
...(type === undefined ? {} : { type }),
|
|
556
|
+
span: spanFromPositions(
|
|
557
|
+
this.#file,
|
|
558
|
+
memberName.span.start,
|
|
559
|
+
(type?.span ?? memberName.span).end,
|
|
560
|
+
),
|
|
561
|
+
nameSpan: memberName.span,
|
|
562
|
+
});
|
|
563
|
+
if (this.#peek().text !== ',') break;
|
|
564
|
+
this.#take();
|
|
565
|
+
if (this.#peek().text === ')') break;
|
|
566
|
+
}
|
|
567
|
+
const groupClose = this.#expectText(')');
|
|
568
|
+
if (members.length < 2) {
|
|
569
|
+
this.#fail(
|
|
570
|
+
'SYQL2011_INVALID_PARAMETER',
|
|
571
|
+
name,
|
|
572
|
+
`group ${name.text} must contain at least two members`,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
parameters.push({
|
|
576
|
+
kind: 'group',
|
|
577
|
+
name: name.text,
|
|
578
|
+
optional: true,
|
|
579
|
+
members,
|
|
580
|
+
span: spanFromPositions(
|
|
581
|
+
this.#file,
|
|
582
|
+
name.span.start,
|
|
583
|
+
groupClose.span.end,
|
|
584
|
+
),
|
|
585
|
+
nameSpan: name.span,
|
|
586
|
+
});
|
|
587
|
+
} else {
|
|
588
|
+
if (publicNames.has(name.text)) {
|
|
589
|
+
this.#duplicate(name, 'public query input');
|
|
590
|
+
}
|
|
591
|
+
publicNames.add(name.text);
|
|
592
|
+
|
|
593
|
+
if (this.#peek().text === ':') {
|
|
594
|
+
const colon = this.#take();
|
|
595
|
+
const typeName = this.#expectKind('identifier', 'parameter type');
|
|
596
|
+
if (typeName.text === 'switch') {
|
|
597
|
+
if (!optional) {
|
|
598
|
+
this.#fail(
|
|
599
|
+
'SYQL2011_INVALID_PARAMETER',
|
|
600
|
+
typeName,
|
|
601
|
+
`switch ${name.text} must use the exact optional form ${name.text}?: switch`,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
parameters.push({
|
|
605
|
+
kind: 'switch',
|
|
606
|
+
name: name.text,
|
|
607
|
+
optional: true,
|
|
608
|
+
span: spanBetween(name, typeName),
|
|
609
|
+
nameSpan: name.span,
|
|
610
|
+
});
|
|
611
|
+
} else {
|
|
612
|
+
const type = this.#parseValueTypeAfterName(colon, typeName);
|
|
613
|
+
if (bindNames.has(name.text)) {
|
|
614
|
+
this.#duplicate(name, 'query bind');
|
|
615
|
+
}
|
|
616
|
+
bindNames.add(name.text);
|
|
617
|
+
parameters.push({
|
|
618
|
+
kind: 'value',
|
|
619
|
+
name: name.text,
|
|
620
|
+
optional,
|
|
621
|
+
type,
|
|
622
|
+
span: spanFromPositions(
|
|
623
|
+
this.#file,
|
|
624
|
+
name.span.start,
|
|
625
|
+
type.span.end,
|
|
626
|
+
),
|
|
627
|
+
nameSpan: name.span,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
} else {
|
|
631
|
+
if (bindNames.has(name.text)) {
|
|
632
|
+
this.#duplicate(name, 'query bind');
|
|
633
|
+
}
|
|
634
|
+
bindNames.add(name.text);
|
|
635
|
+
parameters.push({
|
|
636
|
+
kind: 'value',
|
|
637
|
+
name: name.text,
|
|
638
|
+
optional,
|
|
639
|
+
span: spanFromPositions(
|
|
640
|
+
this.#file,
|
|
641
|
+
name.span.start,
|
|
642
|
+
optional
|
|
643
|
+
? (this.#previousSignificant() as SyqlToken).span.end
|
|
644
|
+
: name.span.end,
|
|
645
|
+
),
|
|
646
|
+
nameSpan: name.span,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (this.#peek().text !== ',') break;
|
|
652
|
+
this.#take();
|
|
653
|
+
if (this.#peek().text === ')') break;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
this.#expectText(')');
|
|
657
|
+
return { parameters, publicNames, bindNames };
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
#parseSqlSection(): SyqlSqlSection {
|
|
661
|
+
const start = this.#expectText('sql');
|
|
662
|
+
const block = this.#parseTemplateBlock('sql section', 'statement');
|
|
663
|
+
return {
|
|
664
|
+
kind: 'sql',
|
|
665
|
+
body: block.template,
|
|
666
|
+
span: spanBetween(start, block.close),
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
#parseSortSection(publicNames: Set<string>): SyqlSortSection {
|
|
671
|
+
const start = this.#expectText('sort');
|
|
672
|
+
const control = this.#parseCamelName('sort control');
|
|
673
|
+
if (publicNames.has(control.text)) {
|
|
674
|
+
this.#duplicate(control, 'public query input');
|
|
675
|
+
}
|
|
676
|
+
publicNames.add(control.text);
|
|
677
|
+
this.#expectText('default');
|
|
678
|
+
const defaultProfile = this.#parseCamelName('default sort profile');
|
|
679
|
+
this.#expectText('{');
|
|
680
|
+
|
|
681
|
+
const profiles: SyqlSortProfile[] = [];
|
|
682
|
+
const profileNames = new Set<string>();
|
|
683
|
+
while (this.#peek().text !== '}') {
|
|
684
|
+
if (this.#peek().kind === 'eof') {
|
|
685
|
+
this.#fail(
|
|
686
|
+
'SYQL2001_EXPECTED_TOKEN',
|
|
687
|
+
this.#peek(),
|
|
688
|
+
'expected } to close sort section',
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
const profileName = this.#parseCamelName('sort profile');
|
|
692
|
+
if (profileNames.has(profileName.text)) {
|
|
693
|
+
this.#duplicate(profileName, 'sort profile');
|
|
694
|
+
}
|
|
695
|
+
profileNames.add(profileName.text);
|
|
696
|
+
const order = this.#parseTemplateBlock('sort profile', 'order');
|
|
697
|
+
profiles.push({
|
|
698
|
+
name: profileName.text,
|
|
699
|
+
order: order.template,
|
|
700
|
+
span: spanBetween(profileName, order.close),
|
|
701
|
+
nameSpan: profileName.span,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
const close = this.#expectText('}');
|
|
705
|
+
if (profiles.length === 0) {
|
|
706
|
+
this.#fail(
|
|
707
|
+
'SYQL2008_INVALID_MEMBER',
|
|
708
|
+
close,
|
|
709
|
+
'a sort section must contain at least one profile',
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
if (!profileNames.has(defaultProfile.text)) {
|
|
713
|
+
this.#fail(
|
|
714
|
+
'SYQL2008_INVALID_MEMBER',
|
|
715
|
+
defaultProfile,
|
|
716
|
+
`default sort profile ${JSON.stringify(defaultProfile.text)} is not declared`,
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
return {
|
|
720
|
+
kind: 'sort',
|
|
721
|
+
control: control.text,
|
|
722
|
+
defaultProfile: defaultProfile.text,
|
|
723
|
+
profiles,
|
|
724
|
+
span: spanBetween(start, close),
|
|
725
|
+
controlSpan: control.span,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
#parsePageDeclaration(publicNames: Set<string>): SyqlPageDeclaration {
|
|
730
|
+
const start = this.#expectText('page');
|
|
731
|
+
const control = this.#parseCamelName('page control');
|
|
732
|
+
if (publicNames.has(control.text)) {
|
|
733
|
+
this.#duplicate(control, 'public query input');
|
|
734
|
+
}
|
|
735
|
+
publicNames.add(control.text);
|
|
736
|
+
this.#expectText('default');
|
|
737
|
+
const defaultToken = this.#parseInteger('page default');
|
|
738
|
+
this.#expectText('max');
|
|
739
|
+
const maxToken = this.#parseInteger('page maximum');
|
|
740
|
+
const end = this.#expectText(';');
|
|
741
|
+
const defaultSize = Number(defaultToken.text);
|
|
742
|
+
const maxSize = Number(maxToken.text);
|
|
743
|
+
if (defaultSize < 1 || defaultSize > maxSize || maxSize > 2_147_483_647) {
|
|
744
|
+
this.#fail(
|
|
745
|
+
'SYQL2010_INVALID_PAGE_RANGE',
|
|
746
|
+
defaultToken,
|
|
747
|
+
`page bounds must satisfy 1 <= default <= max <= 2147483647; found default ${defaultSize}, max ${maxSize}`,
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
kind: 'page',
|
|
752
|
+
control: control.text,
|
|
753
|
+
defaultSize,
|
|
754
|
+
maxSize,
|
|
755
|
+
span: spanBetween(start, end),
|
|
756
|
+
controlSpan: control.span,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
#parseIdentityDeclaration(): SyqlIdentityDeclaration {
|
|
761
|
+
const start = this.#expectText('identity');
|
|
762
|
+
this.#expectText('by');
|
|
763
|
+
const fields: string[] = [];
|
|
764
|
+
const names = new Set<string>();
|
|
765
|
+
for (;;) {
|
|
766
|
+
const field = this.#expectKind('identifier', 'identity result name');
|
|
767
|
+
if (!IDENT_RE.test(field.text)) {
|
|
768
|
+
this.#fail(
|
|
769
|
+
'SYQL2002_INVALID_NAME',
|
|
770
|
+
field,
|
|
771
|
+
`invalid identity result name ${JSON.stringify(field.text)}`,
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
if (names.has(field.text)) this.#duplicate(field, 'identity field');
|
|
775
|
+
names.add(field.text);
|
|
776
|
+
fields.push(field.text);
|
|
777
|
+
if (this.#peek().text !== ',') break;
|
|
778
|
+
this.#take();
|
|
779
|
+
}
|
|
780
|
+
const end = this.#expectText(';');
|
|
781
|
+
return { kind: 'identity', fields, span: spanBetween(start, end) };
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
#parseTypeAnnotation(): SyqlValueType {
|
|
785
|
+
const colon = this.#expectText(':');
|
|
786
|
+
const name = this.#expectKind('identifier', 'value type');
|
|
787
|
+
return this.#parseValueTypeAfterName(colon, name);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
#parseValueTypeAfterName(colon: SyqlToken, name: SyqlToken): SyqlValueType {
|
|
791
|
+
if (!BASE_TYPES.has(name.text as SyqlBaseType)) {
|
|
792
|
+
this.#fail(
|
|
793
|
+
'SYQL2011_INVALID_PARAMETER',
|
|
794
|
+
name,
|
|
795
|
+
`unknown value type ${JSON.stringify(name.text)}`,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
let end = name;
|
|
799
|
+
let nullable = false;
|
|
800
|
+
if (this.#peek().text === '|') {
|
|
801
|
+
this.#take();
|
|
802
|
+
end = this.#expectText('null');
|
|
803
|
+
nullable = true;
|
|
804
|
+
}
|
|
805
|
+
return {
|
|
806
|
+
base: name.text as SyqlBaseType,
|
|
807
|
+
nullable,
|
|
808
|
+
span: spanBetween(colon, end),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
#parseInteger(label: string): SyqlToken {
|
|
813
|
+
const token = this.#take();
|
|
814
|
+
if (token.kind !== 'number' || !INTEGER_LITERAL_RE.test(token.text)) {
|
|
815
|
+
this.#fail(
|
|
816
|
+
'SYQL2009_INVALID_INTEGER',
|
|
817
|
+
token,
|
|
818
|
+
`${label} must be a decimal integer literal without sign or separators`,
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
return token;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
#parseTemplateBlock(label: string, mode: SyqlTemplateMode): ParsedBlock {
|
|
825
|
+
const open = this.#expectText('{');
|
|
826
|
+
const contentStart = this.#index;
|
|
827
|
+
let depth = 1;
|
|
828
|
+
let cursor = this.#index;
|
|
829
|
+
while (cursor < this.#tokens.length) {
|
|
830
|
+
const token = this.#tokens[cursor] as SyqlToken;
|
|
831
|
+
if (token.kind === 'eof') {
|
|
832
|
+
this.#fail(
|
|
833
|
+
'SYQL2001_EXPECTED_TOKEN',
|
|
834
|
+
token,
|
|
835
|
+
`expected } to close ${label}`,
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
if (token.kind === 'punctuation') {
|
|
839
|
+
if (token.text === '{') depth += 1;
|
|
840
|
+
else if (token.text === '}') {
|
|
841
|
+
depth -= 1;
|
|
842
|
+
if (depth === 0) {
|
|
843
|
+
const bodyTokens = this.#tokens.slice(contentStart, cursor);
|
|
844
|
+
const semanticTokens = bodyTokens.filter(
|
|
845
|
+
(candidate) => !isSyqlTrivia(candidate),
|
|
846
|
+
);
|
|
847
|
+
if (semanticTokens.length === 0) {
|
|
848
|
+
this.#fail(
|
|
849
|
+
'SYQL2006_EMPTY_TEMPLATE',
|
|
850
|
+
token,
|
|
851
|
+
`${label} must not be empty`,
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
if (
|
|
855
|
+
semanticTokens.some(
|
|
856
|
+
(candidate) =>
|
|
857
|
+
candidate.kind === 'punctuation' && candidate.text === ';',
|
|
858
|
+
)
|
|
859
|
+
) {
|
|
860
|
+
const semicolon = semanticTokens.find(
|
|
861
|
+
(candidate) =>
|
|
862
|
+
candidate.kind === 'punctuation' && candidate.text === ';',
|
|
863
|
+
) as SyqlToken;
|
|
864
|
+
this.#fail(
|
|
865
|
+
'SYQL2007_FORBIDDEN_SEMICOLON',
|
|
866
|
+
semicolon,
|
|
867
|
+
`${label} must not contain a semicolon token`,
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
const templateSpan = spanFromPositions(
|
|
871
|
+
this.#file,
|
|
872
|
+
open.span.end,
|
|
873
|
+
token.span.start,
|
|
874
|
+
);
|
|
875
|
+
const tree = parseSyqlEmbeddedTemplate(
|
|
876
|
+
this.#file,
|
|
877
|
+
bodyTokens,
|
|
878
|
+
templateSpan,
|
|
879
|
+
mode,
|
|
880
|
+
);
|
|
881
|
+
this.#index = cursor + 1;
|
|
882
|
+
return {
|
|
883
|
+
template: {
|
|
884
|
+
text: this.#source.slice(
|
|
885
|
+
open.span.end.offset,
|
|
886
|
+
token.span.start.offset,
|
|
887
|
+
),
|
|
888
|
+
tokens: bodyTokens,
|
|
889
|
+
tree,
|
|
890
|
+
span: templateSpan,
|
|
891
|
+
},
|
|
892
|
+
open,
|
|
893
|
+
close: token,
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
cursor += 1;
|
|
899
|
+
}
|
|
900
|
+
throw new Error('unreachable: token stream always ends in EOF');
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
#parseCamelName(label: string): SyqlToken {
|
|
904
|
+
const token = this.#expectKind('identifier', label);
|
|
905
|
+
if (!CAMEL_IDENT_RE.test(token.text)) {
|
|
906
|
+
this.#fail(
|
|
907
|
+
'SYQL2002_INVALID_NAME',
|
|
908
|
+
token,
|
|
909
|
+
`${label} must match [a-z][A-Za-z0-9]*; found ${JSON.stringify(token.text)}`,
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
if (token.text.toLowerCase().startsWith('__syql')) {
|
|
913
|
+
this.#fail(
|
|
914
|
+
'SYQL2003_RESERVED_NAME',
|
|
915
|
+
token,
|
|
916
|
+
`${JSON.stringify(token.text)} uses the reserved __syql prefix`,
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
if (RESERVED_NAMES.has(token.text)) {
|
|
920
|
+
this.#fail(
|
|
921
|
+
'SYQL2003_RESERVED_NAME',
|
|
922
|
+
token,
|
|
923
|
+
`${JSON.stringify(token.text)} is a reserved SYQL name`,
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
return token;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
#claimName(
|
|
930
|
+
names: Map<string, SyqlSourceSpan>,
|
|
931
|
+
name: string,
|
|
932
|
+
span: SyqlSourceSpan,
|
|
933
|
+
label: string,
|
|
934
|
+
): void {
|
|
935
|
+
if (names.has(name)) {
|
|
936
|
+
throw new SyqlFrontendError(
|
|
937
|
+
'SYQL2004_DUPLICATE_NAME',
|
|
938
|
+
span,
|
|
939
|
+
`duplicate ${label} ${JSON.stringify(name)}`,
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
names.set(name, span);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
#duplicate(token: SyqlToken, label: string): never {
|
|
946
|
+
this.#fail(
|
|
947
|
+
'SYQL2004_DUPLICATE_NAME',
|
|
948
|
+
token,
|
|
949
|
+
`duplicate ${label} ${JSON.stringify(token.text)}`,
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
#peek(): SyqlToken {
|
|
954
|
+
let index = this.#index;
|
|
955
|
+
while (
|
|
956
|
+
index < this.#tokens.length &&
|
|
957
|
+
isSyqlTrivia(this.#tokens[index] as SyqlToken)
|
|
958
|
+
) {
|
|
959
|
+
index += 1;
|
|
960
|
+
}
|
|
961
|
+
return this.#tokens[index] as SyqlToken;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
#take(): SyqlToken {
|
|
965
|
+
while (
|
|
966
|
+
this.#index < this.#tokens.length &&
|
|
967
|
+
isSyqlTrivia(this.#tokens[this.#index] as SyqlToken)
|
|
968
|
+
) {
|
|
969
|
+
this.#index += 1;
|
|
970
|
+
}
|
|
971
|
+
const token = this.#tokens[this.#index] as SyqlToken;
|
|
972
|
+
this.#index += 1;
|
|
973
|
+
return token;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
#previousSignificant(): SyqlToken | undefined {
|
|
977
|
+
let index = this.#index - 1;
|
|
978
|
+
while (index >= 0) {
|
|
979
|
+
const token = this.#tokens[index] as SyqlToken;
|
|
980
|
+
if (!isSyqlTrivia(token)) return token;
|
|
981
|
+
index -= 1;
|
|
982
|
+
}
|
|
983
|
+
return undefined;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
#expectText(text: string): SyqlToken {
|
|
987
|
+
const token = this.#take();
|
|
988
|
+
if (token.text !== text) {
|
|
989
|
+
this.#fail(
|
|
990
|
+
'SYQL2001_EXPECTED_TOKEN',
|
|
991
|
+
token,
|
|
992
|
+
`expected ${JSON.stringify(text)}, found ${JSON.stringify(token.text || 'end of file')}`,
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
return token;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
#expectKind(kind: SyqlToken['kind'], label: string): SyqlToken {
|
|
999
|
+
const token = this.#take();
|
|
1000
|
+
if (token.kind !== kind) {
|
|
1001
|
+
this.#fail(
|
|
1002
|
+
'SYQL2001_EXPECTED_TOKEN',
|
|
1003
|
+
token,
|
|
1004
|
+
`expected ${label}, found ${JSON.stringify(token.text || 'end of file')}`,
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
return token;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
#fail(code: SyqlParseErrorCode, token: SyqlToken, message: string): never {
|
|
1011
|
+
throw new SyqlFrontendError(code, token.span, message);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/** Parse one `.syql` file using the destructive revision-1 grammar. */
|
|
1016
|
+
export function parseSyqlSyntaxFile(
|
|
1017
|
+
file: string,
|
|
1018
|
+
source: string,
|
|
1019
|
+
): SyqlSyntaxFile {
|
|
1020
|
+
return new Parser(file, source).parse();
|
|
1021
|
+
}
|