@syncular/typegen 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,604 @@
1
+ /** Revision-1 SYQL container parser (`docs/SYQL.md` §3). */
2
+ import { isSyqlTrivia, lexSyqlSource, SyqlFrontendError, } from './syql-lexer.js';
3
+ import { parseSyqlEmbeddedTemplate, } from './syql-template-parser.js';
4
+ const CAMEL_IDENT_RE = /^[a-z][A-Za-z0-9]*$/;
5
+ const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
6
+ const INTEGER_LITERAL_RE = /^(?:0|[1-9][0-9]*)$/;
7
+ const BASE_TYPES = new Set([
8
+ 'string',
9
+ 'integer',
10
+ 'float',
11
+ 'boolean',
12
+ 'json',
13
+ 'bytes',
14
+ 'blob_ref',
15
+ 'crdt',
16
+ ]);
17
+ const RESERVED_NAMES = new Set([
18
+ 'as',
19
+ 'blob_ref',
20
+ 'boolean',
21
+ 'by',
22
+ 'bytes',
23
+ 'cover',
24
+ 'crdt',
25
+ 'default',
26
+ 'float',
27
+ 'from',
28
+ 'identity',
29
+ 'import',
30
+ 'in',
31
+ 'integer',
32
+ 'json',
33
+ 'max',
34
+ 'null',
35
+ 'page',
36
+ 'predicate',
37
+ 'query',
38
+ 'scope',
39
+ 'sort',
40
+ 'sql',
41
+ 'string',
42
+ 'switch',
43
+ 'when',
44
+ ]);
45
+ function spanBetween(start, end) {
46
+ return {
47
+ file: start.span.file,
48
+ start: start.span.start,
49
+ end: end.span.end,
50
+ };
51
+ }
52
+ function spanFromPositions(file, start, end) {
53
+ return { file, start, end };
54
+ }
55
+ class Parser {
56
+ #file;
57
+ #source;
58
+ #tokens;
59
+ #index = 0;
60
+ constructor(file, source) {
61
+ this.#file = file;
62
+ this.#source = source;
63
+ this.#tokens = lexSyqlSource(file, source);
64
+ }
65
+ parse() {
66
+ const imports = [];
67
+ const declarations = [];
68
+ const predicates = [];
69
+ const queries = [];
70
+ const fileNames = new Map();
71
+ let sawDeclaration = false;
72
+ while (this.#peek().kind !== 'eof') {
73
+ const keyword = this.#peek();
74
+ if (keyword.text === 'import') {
75
+ if (sawDeclaration) {
76
+ this.#fail('SYQL2008_INVALID_MEMBER', keyword, 'imports must precede all predicate and query declarations');
77
+ }
78
+ const declaration = this.#parseImport();
79
+ for (const item of declaration.items) {
80
+ this.#claimName(fileNames, item.local, item.span, 'imported name');
81
+ }
82
+ imports.push(declaration);
83
+ continue;
84
+ }
85
+ sawDeclaration = true;
86
+ if (keyword.text === 'predicate') {
87
+ const declaration = this.#parsePredicate();
88
+ this.#claimName(fileNames, declaration.name, declaration.nameSpan, 'declaration');
89
+ declarations.push(declaration);
90
+ predicates.push(declaration);
91
+ }
92
+ else if (keyword.text === 'query') {
93
+ const declaration = this.#parseQuery();
94
+ this.#claimName(fileNames, declaration.name, declaration.nameSpan, 'declaration');
95
+ declarations.push(declaration);
96
+ queries.push(declaration);
97
+ }
98
+ else {
99
+ this.#fail('SYQL2008_INVALID_MEMBER', keyword, `expected import, predicate, or query; found ${JSON.stringify(keyword.text)}`);
100
+ }
101
+ }
102
+ const first = this.#tokens[0];
103
+ const eof = this.#tokens[this.#tokens.length - 1];
104
+ return {
105
+ kind: 'file',
106
+ file: this.#file,
107
+ source: this.#source,
108
+ tokens: this.#tokens,
109
+ imports,
110
+ declarations,
111
+ predicates,
112
+ queries,
113
+ span: spanFromPositions(this.#file, first.span.start, eof.span.end),
114
+ };
115
+ }
116
+ #parseImport() {
117
+ const start = this.#expectText('import');
118
+ this.#expectText('{');
119
+ const items = [];
120
+ const localNames = new Map();
121
+ const importedNames = new Map();
122
+ if (this.#peek().text === '}') {
123
+ this.#fail('SYQL2005_INVALID_IMPORT', this.#peek(), 'an import list must contain at least one predicate name');
124
+ }
125
+ for (;;) {
126
+ const importedToken = this.#parseCamelName('imported predicate name');
127
+ this.#claimName(importedNames, importedToken.text, importedToken.span, 'imported predicate');
128
+ let localToken = importedToken;
129
+ if (this.#peek().text === 'as') {
130
+ this.#take();
131
+ localToken = this.#parseCamelName('import alias');
132
+ }
133
+ this.#claimName(localNames, localToken.text, localToken.span, 'import alias');
134
+ items.push({
135
+ imported: importedToken.text,
136
+ local: localToken.text,
137
+ span: spanBetween(importedToken, localToken),
138
+ });
139
+ if (this.#peek().text !== ',')
140
+ break;
141
+ this.#take();
142
+ if (this.#peek().text === '}')
143
+ break;
144
+ }
145
+ this.#expectText('}');
146
+ this.#expectText('from');
147
+ const pathToken = this.#take();
148
+ if (pathToken.kind !== 'import-path') {
149
+ this.#fail('SYQL2005_INVALID_IMPORT', pathToken, 'expected a JSON double-quoted relative .syql import path');
150
+ }
151
+ let path;
152
+ try {
153
+ path = JSON.parse(pathToken.text);
154
+ }
155
+ catch {
156
+ this.#fail('SYQL2005_INVALID_IMPORT', pathToken, 'invalid JSON import path');
157
+ }
158
+ if (typeof path !== 'string' ||
159
+ (!path.startsWith('./') && !path.startsWith('../')) ||
160
+ !path.endsWith('.syql') ||
161
+ path.includes('\0') ||
162
+ path.includes('\\')) {
163
+ this.#fail('SYQL2005_INVALID_IMPORT', pathToken, 'import path must be a slash-separated relative path ending in .syql');
164
+ }
165
+ const end = this.#expectText(';');
166
+ return { kind: 'import', items, path, span: spanBetween(start, end) };
167
+ }
168
+ #parsePredicate() {
169
+ const start = this.#expectText('predicate');
170
+ const name = this.#parseCamelName('predicate name');
171
+ this.#expectText('(');
172
+ const parameters = [];
173
+ const names = new Map();
174
+ if (this.#peek().text !== ')') {
175
+ for (;;) {
176
+ const paramStart = this.#parseCamelName('predicate parameter');
177
+ const type = this.#peek().text === ':' ? this.#parseTypeAnnotation() : undefined;
178
+ const span = spanFromPositions(this.#file, paramStart.span.start, (type?.span ?? paramStart.span).end);
179
+ this.#claimName(names, paramStart.text, paramStart.span, 'predicate parameter');
180
+ parameters.push({
181
+ name: paramStart.text,
182
+ ...(type === undefined ? {} : { type }),
183
+ span,
184
+ nameSpan: paramStart.span,
185
+ });
186
+ if (this.#peek().text !== ',')
187
+ break;
188
+ this.#take();
189
+ if (this.#peek().text === ')')
190
+ break;
191
+ }
192
+ }
193
+ this.#expectText(')');
194
+ const body = this.#parseTemplateBlock('predicate body', 'predicate');
195
+ return {
196
+ kind: 'predicate',
197
+ name: name.text,
198
+ parameters,
199
+ body: body.template,
200
+ span: spanBetween(start, body.close),
201
+ nameSpan: name.span,
202
+ };
203
+ }
204
+ #parseQuery() {
205
+ const start = this.#expectText('query');
206
+ const name = this.#parseCamelName('query name');
207
+ const parsedParameters = this.#parseQueryParameters();
208
+ const publicNames = parsedParameters.publicNames;
209
+ this.#expectText('{');
210
+ if (this.#peek().text !== 'sql') {
211
+ this.#fail('SYQL2008_INVALID_MEMBER', this.#peek(), 'a query body must begin with exactly one sql { ... } section');
212
+ }
213
+ const sql = this.#parseSqlSection();
214
+ let sort;
215
+ let page;
216
+ let identity;
217
+ if (this.#peek().text === 'sort') {
218
+ sort = this.#parseSortSection(publicNames);
219
+ }
220
+ if (this.#peek().text === 'page') {
221
+ page = this.#parsePageDeclaration(publicNames);
222
+ }
223
+ if (this.#peek().text === 'identity') {
224
+ identity = this.#parseIdentityDeclaration();
225
+ }
226
+ if (this.#peek().text !== '}') {
227
+ this.#fail('SYQL2008_INVALID_MEMBER', this.#peek(), `unexpected or out-of-order query member ${JSON.stringify(this.#peek().text)}`);
228
+ }
229
+ const close = this.#expectText('}');
230
+ return {
231
+ kind: 'query',
232
+ name: name.text,
233
+ parameters: parsedParameters.parameters,
234
+ sql,
235
+ ...(sort === undefined ? {} : { sort }),
236
+ ...(page === undefined ? {} : { page }),
237
+ ...(identity === undefined ? {} : { identity }),
238
+ span: spanBetween(start, close),
239
+ nameSpan: name.span,
240
+ };
241
+ }
242
+ #parseQueryParameters() {
243
+ this.#expectText('(');
244
+ const parameters = [];
245
+ const publicNames = new Set();
246
+ const bindNames = new Set();
247
+ if (this.#peek().text !== ')') {
248
+ for (;;) {
249
+ const name = this.#parseCamelName('query parameter');
250
+ const optional = this.#peek().text === '?';
251
+ if (optional)
252
+ this.#take();
253
+ if (optional && this.#peek().text === '(') {
254
+ if (publicNames.has(name.text)) {
255
+ this.#duplicate(name, 'public query input');
256
+ }
257
+ publicNames.add(name.text);
258
+ this.#take();
259
+ const members = [];
260
+ for (;;) {
261
+ const memberName = this.#parseCamelName('group member');
262
+ if (memberName.text === name.text) {
263
+ this.#fail('SYQL2011_INVALID_PARAMETER', memberName, `group ${name.text} cannot contain a member with the same name`);
264
+ }
265
+ if (bindNames.has(memberName.text)) {
266
+ this.#duplicate(memberName, 'query bind');
267
+ }
268
+ bindNames.add(memberName.text);
269
+ const type = this.#peek().text === ':'
270
+ ? this.#parseTypeAnnotation()
271
+ : undefined;
272
+ members.push({
273
+ name: memberName.text,
274
+ ...(type === undefined ? {} : { type }),
275
+ span: spanFromPositions(this.#file, memberName.span.start, (type?.span ?? memberName.span).end),
276
+ nameSpan: memberName.span,
277
+ });
278
+ if (this.#peek().text !== ',')
279
+ break;
280
+ this.#take();
281
+ if (this.#peek().text === ')')
282
+ break;
283
+ }
284
+ const groupClose = this.#expectText(')');
285
+ if (members.length < 2) {
286
+ this.#fail('SYQL2011_INVALID_PARAMETER', name, `group ${name.text} must contain at least two members`);
287
+ }
288
+ parameters.push({
289
+ kind: 'group',
290
+ name: name.text,
291
+ optional: true,
292
+ members,
293
+ span: spanFromPositions(this.#file, name.span.start, groupClose.span.end),
294
+ nameSpan: name.span,
295
+ });
296
+ }
297
+ else {
298
+ if (publicNames.has(name.text)) {
299
+ this.#duplicate(name, 'public query input');
300
+ }
301
+ publicNames.add(name.text);
302
+ if (this.#peek().text === ':') {
303
+ const colon = this.#take();
304
+ const typeName = this.#expectKind('identifier', 'parameter type');
305
+ if (typeName.text === 'switch') {
306
+ if (!optional) {
307
+ this.#fail('SYQL2011_INVALID_PARAMETER', typeName, `switch ${name.text} must use the exact optional form ${name.text}?: switch`);
308
+ }
309
+ parameters.push({
310
+ kind: 'switch',
311
+ name: name.text,
312
+ optional: true,
313
+ span: spanBetween(name, typeName),
314
+ nameSpan: name.span,
315
+ });
316
+ }
317
+ else {
318
+ const type = this.#parseValueTypeAfterName(colon, typeName);
319
+ if (bindNames.has(name.text)) {
320
+ this.#duplicate(name, 'query bind');
321
+ }
322
+ bindNames.add(name.text);
323
+ parameters.push({
324
+ kind: 'value',
325
+ name: name.text,
326
+ optional,
327
+ type,
328
+ span: spanFromPositions(this.#file, name.span.start, type.span.end),
329
+ nameSpan: name.span,
330
+ });
331
+ }
332
+ }
333
+ else {
334
+ if (bindNames.has(name.text)) {
335
+ this.#duplicate(name, 'query bind');
336
+ }
337
+ bindNames.add(name.text);
338
+ parameters.push({
339
+ kind: 'value',
340
+ name: name.text,
341
+ optional,
342
+ span: spanFromPositions(this.#file, name.span.start, optional
343
+ ? this.#previousSignificant().span.end
344
+ : name.span.end),
345
+ nameSpan: name.span,
346
+ });
347
+ }
348
+ }
349
+ if (this.#peek().text !== ',')
350
+ break;
351
+ this.#take();
352
+ if (this.#peek().text === ')')
353
+ break;
354
+ }
355
+ }
356
+ this.#expectText(')');
357
+ return { parameters, publicNames, bindNames };
358
+ }
359
+ #parseSqlSection() {
360
+ const start = this.#expectText('sql');
361
+ const block = this.#parseTemplateBlock('sql section', 'statement');
362
+ return {
363
+ kind: 'sql',
364
+ body: block.template,
365
+ span: spanBetween(start, block.close),
366
+ };
367
+ }
368
+ #parseSortSection(publicNames) {
369
+ const start = this.#expectText('sort');
370
+ const control = this.#parseCamelName('sort control');
371
+ if (publicNames.has(control.text)) {
372
+ this.#duplicate(control, 'public query input');
373
+ }
374
+ publicNames.add(control.text);
375
+ this.#expectText('default');
376
+ const defaultProfile = this.#parseCamelName('default sort profile');
377
+ this.#expectText('{');
378
+ const profiles = [];
379
+ const profileNames = new Set();
380
+ while (this.#peek().text !== '}') {
381
+ if (this.#peek().kind === 'eof') {
382
+ this.#fail('SYQL2001_EXPECTED_TOKEN', this.#peek(), 'expected } to close sort section');
383
+ }
384
+ const profileName = this.#parseCamelName('sort profile');
385
+ if (profileNames.has(profileName.text)) {
386
+ this.#duplicate(profileName, 'sort profile');
387
+ }
388
+ profileNames.add(profileName.text);
389
+ const order = this.#parseTemplateBlock('sort profile', 'order');
390
+ profiles.push({
391
+ name: profileName.text,
392
+ order: order.template,
393
+ span: spanBetween(profileName, order.close),
394
+ nameSpan: profileName.span,
395
+ });
396
+ }
397
+ const close = this.#expectText('}');
398
+ if (profiles.length === 0) {
399
+ this.#fail('SYQL2008_INVALID_MEMBER', close, 'a sort section must contain at least one profile');
400
+ }
401
+ if (!profileNames.has(defaultProfile.text)) {
402
+ this.#fail('SYQL2008_INVALID_MEMBER', defaultProfile, `default sort profile ${JSON.stringify(defaultProfile.text)} is not declared`);
403
+ }
404
+ return {
405
+ kind: 'sort',
406
+ control: control.text,
407
+ defaultProfile: defaultProfile.text,
408
+ profiles,
409
+ span: spanBetween(start, close),
410
+ controlSpan: control.span,
411
+ };
412
+ }
413
+ #parsePageDeclaration(publicNames) {
414
+ const start = this.#expectText('page');
415
+ const control = this.#parseCamelName('page control');
416
+ if (publicNames.has(control.text)) {
417
+ this.#duplicate(control, 'public query input');
418
+ }
419
+ publicNames.add(control.text);
420
+ this.#expectText('default');
421
+ const defaultToken = this.#parseInteger('page default');
422
+ this.#expectText('max');
423
+ const maxToken = this.#parseInteger('page maximum');
424
+ const end = this.#expectText(';');
425
+ const defaultSize = Number(defaultToken.text);
426
+ const maxSize = Number(maxToken.text);
427
+ if (defaultSize < 1 || defaultSize > maxSize || maxSize > 2_147_483_647) {
428
+ this.#fail('SYQL2010_INVALID_PAGE_RANGE', defaultToken, `page bounds must satisfy 1 <= default <= max <= 2147483647; found default ${defaultSize}, max ${maxSize}`);
429
+ }
430
+ return {
431
+ kind: 'page',
432
+ control: control.text,
433
+ defaultSize,
434
+ maxSize,
435
+ span: spanBetween(start, end),
436
+ controlSpan: control.span,
437
+ };
438
+ }
439
+ #parseIdentityDeclaration() {
440
+ const start = this.#expectText('identity');
441
+ this.#expectText('by');
442
+ const fields = [];
443
+ const names = new Set();
444
+ for (;;) {
445
+ const field = this.#expectKind('identifier', 'identity result name');
446
+ if (!IDENT_RE.test(field.text)) {
447
+ this.#fail('SYQL2002_INVALID_NAME', field, `invalid identity result name ${JSON.stringify(field.text)}`);
448
+ }
449
+ if (names.has(field.text))
450
+ this.#duplicate(field, 'identity field');
451
+ names.add(field.text);
452
+ fields.push(field.text);
453
+ if (this.#peek().text !== ',')
454
+ break;
455
+ this.#take();
456
+ }
457
+ const end = this.#expectText(';');
458
+ return { kind: 'identity', fields, span: spanBetween(start, end) };
459
+ }
460
+ #parseTypeAnnotation() {
461
+ const colon = this.#expectText(':');
462
+ const name = this.#expectKind('identifier', 'value type');
463
+ return this.#parseValueTypeAfterName(colon, name);
464
+ }
465
+ #parseValueTypeAfterName(colon, name) {
466
+ if (!BASE_TYPES.has(name.text)) {
467
+ this.#fail('SYQL2011_INVALID_PARAMETER', name, `unknown value type ${JSON.stringify(name.text)}`);
468
+ }
469
+ let end = name;
470
+ let nullable = false;
471
+ if (this.#peek().text === '|') {
472
+ this.#take();
473
+ end = this.#expectText('null');
474
+ nullable = true;
475
+ }
476
+ return {
477
+ base: name.text,
478
+ nullable,
479
+ span: spanBetween(colon, end),
480
+ };
481
+ }
482
+ #parseInteger(label) {
483
+ const token = this.#take();
484
+ if (token.kind !== 'number' || !INTEGER_LITERAL_RE.test(token.text)) {
485
+ this.#fail('SYQL2009_INVALID_INTEGER', token, `${label} must be a decimal integer literal without sign or separators`);
486
+ }
487
+ return token;
488
+ }
489
+ #parseTemplateBlock(label, mode) {
490
+ const open = this.#expectText('{');
491
+ const contentStart = this.#index;
492
+ let depth = 1;
493
+ let cursor = this.#index;
494
+ while (cursor < this.#tokens.length) {
495
+ const token = this.#tokens[cursor];
496
+ if (token.kind === 'eof') {
497
+ this.#fail('SYQL2001_EXPECTED_TOKEN', token, `expected } to close ${label}`);
498
+ }
499
+ if (token.kind === 'punctuation') {
500
+ if (token.text === '{')
501
+ depth += 1;
502
+ else if (token.text === '}') {
503
+ depth -= 1;
504
+ if (depth === 0) {
505
+ const bodyTokens = this.#tokens.slice(contentStart, cursor);
506
+ const semanticTokens = bodyTokens.filter((candidate) => !isSyqlTrivia(candidate));
507
+ if (semanticTokens.length === 0) {
508
+ this.#fail('SYQL2006_EMPTY_TEMPLATE', token, `${label} must not be empty`);
509
+ }
510
+ if (semanticTokens.some((candidate) => candidate.kind === 'punctuation' && candidate.text === ';')) {
511
+ const semicolon = semanticTokens.find((candidate) => candidate.kind === 'punctuation' && candidate.text === ';');
512
+ this.#fail('SYQL2007_FORBIDDEN_SEMICOLON', semicolon, `${label} must not contain a semicolon token`);
513
+ }
514
+ const templateSpan = spanFromPositions(this.#file, open.span.end, token.span.start);
515
+ const tree = parseSyqlEmbeddedTemplate(this.#file, bodyTokens, templateSpan, mode);
516
+ this.#index = cursor + 1;
517
+ return {
518
+ template: {
519
+ text: this.#source.slice(open.span.end.offset, token.span.start.offset),
520
+ tokens: bodyTokens,
521
+ tree,
522
+ span: templateSpan,
523
+ },
524
+ open,
525
+ close: token,
526
+ };
527
+ }
528
+ }
529
+ }
530
+ cursor += 1;
531
+ }
532
+ throw new Error('unreachable: token stream always ends in EOF');
533
+ }
534
+ #parseCamelName(label) {
535
+ const token = this.#expectKind('identifier', label);
536
+ if (!CAMEL_IDENT_RE.test(token.text)) {
537
+ this.#fail('SYQL2002_INVALID_NAME', token, `${label} must match [a-z][A-Za-z0-9]*; found ${JSON.stringify(token.text)}`);
538
+ }
539
+ if (token.text.toLowerCase().startsWith('__syql')) {
540
+ this.#fail('SYQL2003_RESERVED_NAME', token, `${JSON.stringify(token.text)} uses the reserved __syql prefix`);
541
+ }
542
+ if (RESERVED_NAMES.has(token.text)) {
543
+ this.#fail('SYQL2003_RESERVED_NAME', token, `${JSON.stringify(token.text)} is a reserved SYQL name`);
544
+ }
545
+ return token;
546
+ }
547
+ #claimName(names, name, span, label) {
548
+ if (names.has(name)) {
549
+ throw new SyqlFrontendError('SYQL2004_DUPLICATE_NAME', span, `duplicate ${label} ${JSON.stringify(name)}`);
550
+ }
551
+ names.set(name, span);
552
+ }
553
+ #duplicate(token, label) {
554
+ this.#fail('SYQL2004_DUPLICATE_NAME', token, `duplicate ${label} ${JSON.stringify(token.text)}`);
555
+ }
556
+ #peek() {
557
+ let index = this.#index;
558
+ while (index < this.#tokens.length &&
559
+ isSyqlTrivia(this.#tokens[index])) {
560
+ index += 1;
561
+ }
562
+ return this.#tokens[index];
563
+ }
564
+ #take() {
565
+ while (this.#index < this.#tokens.length &&
566
+ isSyqlTrivia(this.#tokens[this.#index])) {
567
+ this.#index += 1;
568
+ }
569
+ const token = this.#tokens[this.#index];
570
+ this.#index += 1;
571
+ return token;
572
+ }
573
+ #previousSignificant() {
574
+ let index = this.#index - 1;
575
+ while (index >= 0) {
576
+ const token = this.#tokens[index];
577
+ if (!isSyqlTrivia(token))
578
+ return token;
579
+ index -= 1;
580
+ }
581
+ return undefined;
582
+ }
583
+ #expectText(text) {
584
+ const token = this.#take();
585
+ if (token.text !== text) {
586
+ this.#fail('SYQL2001_EXPECTED_TOKEN', token, `expected ${JSON.stringify(text)}, found ${JSON.stringify(token.text || 'end of file')}`);
587
+ }
588
+ return token;
589
+ }
590
+ #expectKind(kind, label) {
591
+ const token = this.#take();
592
+ if (token.kind !== kind) {
593
+ this.#fail('SYQL2001_EXPECTED_TOKEN', token, `expected ${label}, found ${JSON.stringify(token.text || 'end of file')}`);
594
+ }
595
+ return token;
596
+ }
597
+ #fail(code, token, message) {
598
+ throw new SyqlFrontendError(code, token.span, message);
599
+ }
600
+ }
601
+ /** Parse one `.syql` file using the destructive revision-1 grammar. */
602
+ export function parseSyqlSyntaxFile(file, source) {
603
+ return new Parser(file, source).parse();
604
+ }
@@ -0,0 +1,58 @@
1
+ /** Embedded SQL-template node parser for revision-1 SYQL (§§6, 8, and 10). */
2
+ import { type SyqlSourceSpan, type SyqlToken } from './syql-lexer.js';
3
+ export type SyqlTemplateMode = 'statement' | 'predicate' | 'condition' | 'order';
4
+ export interface SyqlRawTemplateNode {
5
+ readonly kind: 'raw';
6
+ readonly text: string;
7
+ readonly tokens: readonly SyqlToken[];
8
+ readonly span: SyqlSourceSpan;
9
+ }
10
+ export interface SyqlBindReference {
11
+ readonly kind: 'bind-reference';
12
+ readonly name: string;
13
+ readonly token: SyqlToken;
14
+ readonly span: SyqlSourceSpan;
15
+ }
16
+ export interface SyqlPredicateCall {
17
+ readonly kind: 'predicate-call';
18
+ readonly name: string;
19
+ readonly arguments: readonly SyqlBindReference[];
20
+ readonly tokens: readonly SyqlToken[];
21
+ readonly span: SyqlSourceSpan;
22
+ }
23
+ export interface SyqlWhenExpression {
24
+ readonly kind: 'when';
25
+ readonly controls: readonly string[];
26
+ readonly controlSpans: readonly SyqlSourceSpan[];
27
+ readonly body: SyqlEmbeddedTemplate;
28
+ readonly tokens: readonly SyqlToken[];
29
+ readonly span: SyqlSourceSpan;
30
+ }
31
+ export interface SyqlQualifiedColumn {
32
+ readonly qualifier: string;
33
+ readonly name: string;
34
+ readonly span: SyqlSourceSpan;
35
+ }
36
+ export interface SyqlScopeBinding {
37
+ readonly kind: 'scope-binding';
38
+ readonly column: SyqlQualifiedColumn;
39
+ readonly operator: 'equal' | 'in';
40
+ readonly values: readonly SyqlBindReference[];
41
+ readonly span: SyqlSourceSpan;
42
+ }
43
+ export interface SyqlReactiveDirective {
44
+ readonly kind: 'scope' | 'cover';
45
+ readonly bindings: readonly SyqlScopeBinding[];
46
+ readonly tokens: readonly SyqlToken[];
47
+ readonly span: SyqlSourceSpan;
48
+ }
49
+ export type SyqlEmbeddedNode = SyqlRawTemplateNode | SyqlPredicateCall | SyqlWhenExpression | SyqlReactiveDirective;
50
+ export interface SyqlEmbeddedTemplate {
51
+ readonly kind: 'template';
52
+ readonly mode: SyqlTemplateMode;
53
+ readonly nodes: readonly SyqlEmbeddedNode[];
54
+ readonly span: SyqlSourceSpan;
55
+ }
56
+ export type SyqlTemplateParseErrorCode = 'SYQL3001_EXPECTED_EMBEDDED_TOKEN' | 'SYQL3002_INVALID_BIND' | 'SYQL3003_INVALID_PREDICATE_CALL' | 'SYQL3004_INVALID_WHEN' | 'SYQL3005_INVALID_REACTIVE_DIRECTIVE' | 'SYQL3006_FORBIDDEN_TEMPLATE_NODE' | 'SYQL3007_UNEXPECTED_BRACE' | 'SYQL3008_FORBIDDEN_PARAMETER_FORM';
57
+ /** Parse embedded nodes from one already-delimited lossless template. */
58
+ export declare function parseSyqlEmbeddedTemplate(file: string, tokens: readonly SyqlToken[], span: SyqlSourceSpan, mode: SyqlTemplateMode): SyqlEmbeddedTemplate;