@syncular/typegen 0.6.0 → 0.8.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.
@@ -2,7 +2,6 @@
2
2
  import { isSyqlTrivia, lexSyqlSource, SyqlFrontendError, } from './syql-lexer.js';
3
3
  import { parseSyqlEmbeddedTemplate, } from './syql-template-parser.js';
4
4
  const CAMEL_IDENT_RE = /^[a-z][A-Za-z0-9]*$/;
5
- const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
6
5
  const INTEGER_LITERAL_RE = /^(?:0|[1-9][0-9]*)$/;
7
6
  const BASE_TYPES = new Set([
8
7
  'string',
@@ -17,29 +16,29 @@ const BASE_TYPES = new Set([
17
16
  const RESERVED_NAMES = new Set([
18
17
  'as',
19
18
  'blob_ref',
19
+ 'bool',
20
20
  'boolean',
21
21
  'by',
22
22
  'bytes',
23
- 'cover',
24
23
  'crdt',
25
24
  'default',
26
25
  'float',
26
+ 'false',
27
27
  'from',
28
- 'identity',
29
28
  'import',
30
29
  'in',
31
30
  'integer',
32
31
  'json',
33
32
  'max',
34
33
  'null',
35
- 'page',
34
+ 'limit',
35
+ 'present',
36
36
  'predicate',
37
37
  'query',
38
- 'scope',
39
38
  'sort',
40
- 'sql',
41
39
  'string',
42
- 'switch',
40
+ 'sync',
41
+ 'true',
43
42
  'when',
44
43
  ]);
45
44
  function spanBetween(start, end) {
@@ -89,14 +88,14 @@ class Parser {
89
88
  declarations.push(declaration);
90
89
  predicates.push(declaration);
91
90
  }
92
- else if (keyword.text === 'query') {
93
- const declaration = this.#parseQuery();
91
+ else if (keyword.text === 'query' || keyword.text === 'sync') {
92
+ const declaration = this.#parseQuery(keyword.text === 'sync');
94
93
  this.#claimName(fileNames, declaration.name, declaration.nameSpan, 'declaration');
95
94
  declarations.push(declaration);
96
95
  queries.push(declaration);
97
96
  }
98
97
  else {
99
- this.#fail('SYQL2008_INVALID_MEMBER', keyword, `expected import, predicate, or query; found ${JSON.stringify(keyword.text)}`);
98
+ this.#fail('SYQL2008_INVALID_MEMBER', keyword, `expected import, predicate, query, or sync query; found ${JSON.stringify(keyword.text)}`);
100
99
  }
101
100
  }
102
101
  const first = this.#tokens[0];
@@ -201,41 +200,64 @@ class Parser {
201
200
  nameSpan: name.span,
202
201
  };
203
202
  }
204
- #parseQuery() {
205
- const start = this.#expectText('query');
203
+ #parseQuery(sync) {
204
+ const start = sync ? this.#expectText('sync') : this.#peek();
205
+ this.#expectText('query');
206
206
  const name = this.#parseCamelName('query name');
207
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)}`);
208
+ let syncBy;
209
+ if (this.#peek().text === 'by') {
210
+ const by = this.#take();
211
+ if (!sync) {
212
+ this.#fail('SYQL2012_INVALID_QUERY_BODY', by, '`by table.scope` is valid only on a sync query');
213
+ }
214
+ const qualifier = this.#expectKind('identifier', 'sync table or alias');
215
+ this.#expectText('.');
216
+ const column = this.#expectKind('identifier', 'sync scope column');
217
+ syncBy = {
218
+ qualifier: qualifier.text,
219
+ column: column.text,
220
+ span: spanBetween(qualifier, column),
221
+ };
222
+ }
223
+ const declaredRanges = new Set(parsedParameters.parameters.flatMap((parameter) => parameter.kind === 'range' ? [parameter.name] : []));
224
+ const body = this.#parseQueryBody(parsedParameters.publicNames, declaredRanges);
225
+ const implicitRanges = this.#rangeShorthandNames(body.statement.tokens, declaredRanges);
226
+ const parameters = parsedParameters.parameters.map((parameter) => {
227
+ if (!implicitRanges.has(parameter.name))
228
+ return parameter;
229
+ if (parameter.kind === 'group') {
230
+ this.#fail('SYQL2011_INVALID_PARAMETER', this.#tokenAt(parameter.nameSpan), `range shorthand :${parameter.name} must name a scalar query input`);
231
+ }
232
+ if (parameter.kind === 'range')
233
+ return parameter;
234
+ if (parameter.default !== undefined) {
235
+ this.#fail('SYQL2011_INVALID_PARAMETER', this.#tokenAt(parameter.nameSpan), `range input ${parameter.name} cannot have a boolean default`);
236
+ }
237
+ return {
238
+ kind: 'range',
239
+ name: parameter.name,
240
+ optional: parameter.optional,
241
+ ...(parameter.type === undefined ? {} : { type: parameter.type }),
242
+ span: parameter.span,
243
+ nameSpan: parameter.nameSpan,
244
+ };
245
+ });
246
+ for (const parameter of parameters) {
247
+ if (parameter.kind === 'range' && !implicitRanges.has(parameter.name)) {
248
+ this.#fail('SYQL2011_INVALID_PARAMETER', this.#tokenAt(parameter.nameSpan), `range input ${parameter.name} must be used as BETWEEN :${parameter.name}`);
249
+ }
228
250
  }
229
- const close = this.#expectText('}');
230
251
  return {
231
252
  kind: 'query',
232
253
  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),
254
+ sync,
255
+ ...(syncBy === undefined ? {} : { syncBy }),
256
+ parameters,
257
+ statement: body.statement,
258
+ ...(body.sort === undefined ? {} : { sort: body.sort }),
259
+ ...(body.limit === undefined ? {} : { limit: body.limit }),
260
+ span: spanBetween(start, body.close),
239
261
  nameSpan: name.span,
240
262
  };
241
263
  }
@@ -250,7 +272,13 @@ class Parser {
250
272
  const optional = this.#peek().text === '?';
251
273
  if (optional)
252
274
  this.#take();
253
- if (optional && this.#peek().text === '(') {
275
+ if (optional &&
276
+ this.#peek().text === ':' &&
277
+ this.#peekSignificantAfter(1)?.text === '{') {
278
+ this.#take();
279
+ if (this.#peek().text !== '{') {
280
+ this.#fail('SYQL2011_INVALID_PARAMETER', this.#peek(), 'an optional structured input must use `name?: { ... }`');
281
+ }
254
282
  if (publicNames.has(name.text)) {
255
283
  this.#duplicate(name, 'public query input');
256
284
  }
@@ -278,10 +306,10 @@ class Parser {
278
306
  if (this.#peek().text !== ',')
279
307
  break;
280
308
  this.#take();
281
- if (this.#peek().text === ')')
309
+ if (this.#peek().text === '}')
282
310
  break;
283
311
  }
284
- const groupClose = this.#expectText(')');
312
+ const groupClose = this.#expectText('}');
285
313
  if (members.length < 2) {
286
314
  this.#fail('SYQL2011_INVALID_PARAMETER', name, `group ${name.text} must contain at least two members`);
287
315
  }
@@ -302,15 +330,21 @@ class Parser {
302
330
  if (this.#peek().text === ':') {
303
331
  const colon = this.#take();
304
332
  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`);
333
+ if (typeName.text === 'range') {
334
+ if (bindNames.has(name.text)) {
335
+ this.#duplicate(name, 'query bind');
308
336
  }
337
+ bindNames.add(name.text);
338
+ this.#expectText('<');
339
+ const elementName = this.#expectKind('identifier', 'range element type');
340
+ const type = this.#parseValueTypeAfterName(colon, elementName);
341
+ const close = this.#expectText('>');
309
342
  parameters.push({
310
- kind: 'switch',
343
+ kind: 'range',
311
344
  name: name.text,
312
- optional: true,
313
- span: spanBetween(name, typeName),
345
+ optional,
346
+ type,
347
+ span: spanBetween(name, close),
314
348
  nameSpan: name.span,
315
349
  });
316
350
  }
@@ -320,12 +354,26 @@ class Parser {
320
354
  this.#duplicate(name, 'query bind');
321
355
  }
322
356
  bindNames.add(name.text);
357
+ let end = type.span.end;
358
+ let defaultValue;
359
+ if (this.#peek().text === '=') {
360
+ this.#take();
361
+ const value = this.#expectText('false');
362
+ if (type.base !== 'boolean' || type.nullable || optional) {
363
+ this.#fail('SYQL2011_INVALID_PARAMETER', value, 'only a non-optional bool parameter may declare `= false`');
364
+ }
365
+ defaultValue = false;
366
+ end = value.span.end;
367
+ }
323
368
  parameters.push({
324
369
  kind: 'value',
325
370
  name: name.text,
326
371
  optional,
327
372
  type,
328
- span: spanFromPositions(this.#file, name.span.start, type.span.end),
373
+ ...(defaultValue === undefined
374
+ ? {}
375
+ : { default: defaultValue }),
376
+ span: spanFromPositions(this.#file, name.span.start, end),
329
377
  nameSpan: name.span,
330
378
  });
331
379
  }
@@ -356,17 +404,201 @@ class Parser {
356
404
  this.#expectText(')');
357
405
  return { parameters, publicNames, bindNames };
358
406
  }
359
- #parseSqlSection() {
360
- const start = this.#expectText('sql');
361
- const block = this.#parseTemplateBlock('sql section', 'statement');
407
+ #parseQueryBody(publicNames, declaredRanges) {
408
+ const open = this.#expectText('{');
409
+ const bodyStart = this.#index;
410
+ let cursor = bodyStart;
411
+ let braceDepth = 1;
412
+ let parenDepth = 0;
413
+ let terminatorIndex;
414
+ while (cursor < this.#tokens.length) {
415
+ const token = this.#tokens[cursor];
416
+ if (token.kind === 'eof') {
417
+ this.#fail('SYQL2001_EXPECTED_TOKEN', token, 'expected ; and } to close query body');
418
+ }
419
+ if (token.text === '(')
420
+ parenDepth += 1;
421
+ else if (token.text === ')')
422
+ parenDepth -= 1;
423
+ else if (token.text === '{')
424
+ braceDepth += 1;
425
+ else if (token.text === '}') {
426
+ if (braceDepth === 1) {
427
+ this.#fail('SYQL2012_INVALID_QUERY_BODY', token, 'a query statement must end with a semicolon');
428
+ }
429
+ braceDepth -= 1;
430
+ }
431
+ else if (token.text === ';' && braceDepth === 1 && parenDepth === 0) {
432
+ terminatorIndex = cursor;
433
+ break;
434
+ }
435
+ cursor += 1;
436
+ }
437
+ const terminator = this.#tokens[terminatorIndex];
438
+ let dynamicSortIndex;
439
+ let dynamicLimitIndex;
440
+ braceDepth = 1;
441
+ parenDepth = 0;
442
+ for (cursor = bodyStart; cursor < terminatorIndex; cursor += 1) {
443
+ const token = this.#tokens[cursor];
444
+ if (isSyqlTrivia(token))
445
+ continue;
446
+ if (token.text === '(')
447
+ parenDepth += 1;
448
+ else if (token.text === ')')
449
+ parenDepth -= 1;
450
+ if (braceDepth === 1 && parenDepth === 0) {
451
+ const lower = token.text.toLowerCase();
452
+ if (lower === 'order' &&
453
+ this.#significantTextAfter(cursor, 1) === 'by' &&
454
+ this.#significantTextAfter(cursor, 3) === 'default' &&
455
+ this.#significantTextAfter(cursor, 5) === '{') {
456
+ dynamicSortIndex ??= cursor;
457
+ }
458
+ else if (lower === 'limit' &&
459
+ this.#significantTextAfter(cursor, 2) === 'default' &&
460
+ this.#significantTextAfter(cursor, 4) === 'max') {
461
+ dynamicLimitIndex ??= cursor;
462
+ }
463
+ }
464
+ if (token.text === '{')
465
+ braceDepth += 1;
466
+ else if (token.text === '}')
467
+ braceDepth -= 1;
468
+ }
469
+ if (dynamicSortIndex !== undefined &&
470
+ dynamicLimitIndex !== undefined &&
471
+ dynamicLimitIndex < dynamicSortIndex) {
472
+ this.#fail('SYQL2012_INVALID_QUERY_BODY', this.#tokens[dynamicLimitIndex], 'dynamic ORDER BY must precede dynamic LIMIT');
473
+ }
474
+ const sqlEnd = Math.min(dynamicSortIndex ?? Number.MAX_SAFE_INTEGER, dynamicLimitIndex ?? Number.MAX_SAFE_INTEGER, terminatorIndex);
475
+ const statementTokens = this.#tokens.slice(bodyStart, sqlEnd);
476
+ const rangeNames = this.#rangeShorthandNames(statementTokens, declaredRanges);
477
+ const statement = this.#makeTemplate(statementTokens, 'query statement', 'statement', rangeNames, open.span.end);
478
+ let sort;
479
+ let limit;
480
+ this.#index = sqlEnd;
481
+ if (dynamicSortIndex !== undefined) {
482
+ sort = this.#parseInlineSort(publicNames);
483
+ }
484
+ if (dynamicLimitIndex !== undefined) {
485
+ limit = this.#parseLimitDeclaration(publicNames);
486
+ }
487
+ const end = this.#expectText(';');
488
+ if (end !== terminator) {
489
+ this.#fail('SYQL2012_INVALID_QUERY_BODY', end, 'unexpected query member before the statement terminator');
490
+ }
491
+ const close = this.#expectText('}');
492
+ return {
493
+ statement,
494
+ ...(sort === undefined ? {} : { sort }),
495
+ ...(limit === undefined ? {} : { limit }),
496
+ close,
497
+ };
498
+ }
499
+ #rangeShorthandNames(tokens, declaredRanges = new Set()) {
500
+ const significant = tokens.filter((token) => !isSyqlTrivia(token));
501
+ const names = new Set();
502
+ const clauseEnd = new Set([
503
+ 'group',
504
+ 'having',
505
+ 'order',
506
+ 'limit',
507
+ 'offset',
508
+ 'window',
509
+ 'union',
510
+ 'intersect',
511
+ 'except',
512
+ ]);
513
+ for (let index = 0; index < significant.length; index += 1) {
514
+ const token = significant[index];
515
+ if (token.kind !== 'identifier' || token.text.toLowerCase() !== 'between')
516
+ continue;
517
+ const bind = significant[index + 1];
518
+ if (bind?.kind !== 'bind')
519
+ continue;
520
+ const name = bind.text.slice(1);
521
+ const next = significant[index + 2];
522
+ const afterNext = significant[index + 3];
523
+ let conjunctStart = index - 1;
524
+ while (conjunctStart >= 0 &&
525
+ !(significant[conjunctStart]?.kind === 'identifier' &&
526
+ significant[conjunctStart]?.text.toLowerCase() === 'and')) {
527
+ conjunctStart -= 1;
528
+ }
529
+ const controlledByRange = significant
530
+ .slice(conjunctStart + 1, index)
531
+ .some((candidate, candidateIndex, conjunct) => {
532
+ if (candidate.kind !== 'identifier' ||
533
+ candidate.text.toLowerCase() !== 'when' ||
534
+ conjunct[candidateIndex + 1]?.text !== '(') {
535
+ return false;
536
+ }
537
+ let depth = 0;
538
+ for (let controlIndex = candidateIndex + 1; controlIndex < conjunct.length; controlIndex += 1) {
539
+ const control = conjunct[controlIndex];
540
+ if (control.text === '(')
541
+ depth += 1;
542
+ else if (control.text === ')') {
543
+ depth -= 1;
544
+ if (depth === 0)
545
+ return false;
546
+ }
547
+ else if (depth > 0 &&
548
+ control.kind === 'identifier' &&
549
+ control.text === name) {
550
+ return true;
551
+ }
552
+ }
553
+ return false;
554
+ });
555
+ const endsRange = declaredRanges.has(name) ||
556
+ controlledByRange ||
557
+ next === undefined ||
558
+ next.text === '}' ||
559
+ (next.kind === 'identifier' &&
560
+ clauseEnd.has(next.text.toLowerCase())) ||
561
+ (next.kind === 'identifier' &&
562
+ next.text.toLowerCase() === 'and' &&
563
+ afterNext?.kind === 'identifier' &&
564
+ afterNext.text === 'when');
565
+ if (endsRange)
566
+ names.add(name);
567
+ }
568
+ return names;
569
+ }
570
+ #makeTemplate(tokens, label, mode, rangeNames = new Set(), fallbackStart) {
571
+ const semantic = tokens.filter((token) => !isSyqlTrivia(token));
572
+ if (semantic.length === 0) {
573
+ this.#fail('SYQL2006_EMPTY_TEMPLATE', semantic[0] ?? this.#peek(), `${label} must not be empty`);
574
+ }
575
+ const first = tokens[0] ?? semantic[0];
576
+ const last = tokens[tokens.length - 1] ?? semantic[semantic.length - 1];
577
+ const start = first?.span.start ?? fallbackStart ?? this.#peek().span.start;
578
+ const end = last?.span.end ?? start;
579
+ const span = spanFromPositions(this.#file, start, end);
362
580
  return {
363
- kind: 'sql',
364
- body: block.template,
365
- span: spanBetween(start, block.close),
581
+ text: this.#source.slice(start.offset, end.offset),
582
+ tokens,
583
+ tree: parseSyqlEmbeddedTemplate(this.#file, tokens, span, mode, rangeNames),
584
+ span,
366
585
  };
367
586
  }
368
- #parseSortSection(publicNames) {
369
- const start = this.#expectText('sort');
587
+ #significantTextAfter(index, count) {
588
+ let remaining = count;
589
+ for (let cursor = index + 1; cursor < this.#tokens.length; cursor += 1) {
590
+ const token = this.#tokens[cursor];
591
+ if (isSyqlTrivia(token))
592
+ continue;
593
+ remaining -= 1;
594
+ if (remaining === 0)
595
+ return token.text.toLowerCase();
596
+ }
597
+ return undefined;
598
+ }
599
+ #parseInlineSort(publicNames) {
600
+ const start = this.#expectText('order');
601
+ this.#expectText('by');
370
602
  const control = this.#parseCamelName('sort control');
371
603
  if (publicNames.has(control.text)) {
372
604
  this.#duplicate(control, 'public query input');
@@ -386,17 +618,37 @@ class Parser {
386
618
  this.#duplicate(profileName, 'sort profile');
387
619
  }
388
620
  profileNames.add(profileName.text);
389
- const order = this.#parseTemplateBlock('sort profile', 'order');
621
+ this.#expectText(':');
622
+ const contentStart = this.#index;
623
+ let cursor = contentStart;
624
+ let depth = 0;
625
+ while (cursor < this.#tokens.length) {
626
+ const token = this.#tokens[cursor];
627
+ if (token.kind === 'eof' || token.text === '}') {
628
+ this.#fail('SYQL2001_EXPECTED_TOKEN', token, 'expected ; after sort profile');
629
+ }
630
+ if (token.text === '(')
631
+ depth += 1;
632
+ else if (token.text === ')')
633
+ depth -= 1;
634
+ else if (token.text === ';' && depth === 0)
635
+ break;
636
+ cursor += 1;
637
+ }
638
+ const end = this.#tokens[cursor];
639
+ const orderTokens = this.#tokens.slice(contentStart, cursor);
640
+ const order = this.#makeTemplate(orderTokens, 'sort profile', 'order');
641
+ this.#index = cursor + 1;
390
642
  profiles.push({
391
643
  name: profileName.text,
392
- order: order.template,
393
- span: spanBetween(profileName, order.close),
644
+ order,
645
+ span: spanBetween(profileName, end),
394
646
  nameSpan: profileName.span,
395
647
  });
396
648
  }
397
649
  const close = this.#expectText('}');
398
650
  if (profiles.length === 0) {
399
- this.#fail('SYQL2008_INVALID_MEMBER', close, 'a sort section must contain at least one profile');
651
+ this.#fail('SYQL2008_INVALID_MEMBER', close, 'a dynamic ORDER BY must contain at least one profile');
400
652
  }
401
653
  if (!profileNames.has(defaultProfile.text)) {
402
654
  this.#fail('SYQL2008_INVALID_MEMBER', defaultProfile, `default sort profile ${JSON.stringify(defaultProfile.text)} is not declared`);
@@ -410,60 +662,39 @@ class Parser {
410
662
  controlSpan: control.span,
411
663
  };
412
664
  }
413
- #parsePageDeclaration(publicNames) {
414
- const start = this.#expectText('page');
415
- const control = this.#parseCamelName('page control');
665
+ #parseLimitDeclaration(publicNames) {
666
+ const start = this.#expectText('limit');
667
+ const control = this.#parseCamelName('limit control');
416
668
  if (publicNames.has(control.text)) {
417
669
  this.#duplicate(control, 'public query input');
418
670
  }
419
671
  publicNames.add(control.text);
420
672
  this.#expectText('default');
421
- const defaultToken = this.#parseInteger('page default');
673
+ const defaultToken = this.#parseInteger('limit default');
422
674
  this.#expectText('max');
423
- const maxToken = this.#parseInteger('page maximum');
424
- const end = this.#expectText(';');
675
+ const maxToken = this.#parseInteger('limit maximum');
425
676
  const defaultSize = Number(defaultToken.text);
426
677
  const maxSize = Number(maxToken.text);
427
678
  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}`);
679
+ this.#fail('SYQL2010_INVALID_PAGE_RANGE', defaultToken, `limit bounds must satisfy 1 <= default <= max <= 2147483647; found default ${defaultSize}, max ${maxSize}`);
429
680
  }
430
681
  return {
431
- kind: 'page',
682
+ kind: 'limit',
432
683
  control: control.text,
433
684
  defaultSize,
434
685
  maxSize,
435
- span: spanBetween(start, end),
686
+ span: spanBetween(start, maxToken),
436
687
  controlSpan: control.span,
437
688
  };
438
689
  }
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
690
  #parseTypeAnnotation() {
461
691
  const colon = this.#expectText(':');
462
692
  const name = this.#expectKind('identifier', 'value type');
463
693
  return this.#parseValueTypeAfterName(colon, name);
464
694
  }
465
695
  #parseValueTypeAfterName(colon, name) {
466
- if (!BASE_TYPES.has(name.text)) {
696
+ const base = name.text === 'bool' ? 'boolean' : name.text;
697
+ if (name.text === 'boolean' || !BASE_TYPES.has(base)) {
467
698
  this.#fail('SYQL2011_INVALID_PARAMETER', name, `unknown value type ${JSON.stringify(name.text)}`);
468
699
  }
469
700
  let end = name;
@@ -474,7 +705,7 @@ class Parser {
474
705
  nullable = true;
475
706
  }
476
707
  return {
477
- base: name.text,
708
+ base: base,
478
709
  nullable,
479
710
  span: spanBetween(colon, end),
480
711
  };
@@ -561,6 +792,20 @@ class Parser {
561
792
  }
562
793
  return this.#tokens[index];
563
794
  }
795
+ #peekSignificantAfter(count) {
796
+ let index = this.#index;
797
+ let remaining = count;
798
+ while (index < this.#tokens.length) {
799
+ const token = this.#tokens[index];
800
+ index += 1;
801
+ if (isSyqlTrivia(token))
802
+ continue;
803
+ if (remaining === 0)
804
+ return token;
805
+ remaining -= 1;
806
+ }
807
+ return undefined;
808
+ }
564
809
  #take() {
565
810
  while (this.#index < this.#tokens.length &&
566
811
  isSyqlTrivia(this.#tokens[this.#index])) {
@@ -580,6 +825,9 @@ class Parser {
580
825
  }
581
826
  return undefined;
582
827
  }
828
+ #tokenAt(span) {
829
+ return (this.#tokens.find((token) => token.span.start.offset === span.start.offset) ?? this.#peek());
830
+ }
583
831
  #expectText(text) {
584
832
  const token = this.#take();
585
833
  if (token.text !== text) {
@@ -8,7 +8,6 @@
8
8
  import { type SyqlSourceSpan } from './syql-lexer.js';
9
9
  import type { SyqlModuleGraph } from './syql-modules.js';
10
10
  import type { SyqlPredicateDeclaration, SyqlQueryDeclaration, SyqlQueryParameter, SyqlSyntaxFile, SyqlValueType } from './syql-parser.js';
11
- import type { SyqlReactiveDirective } from './syql-template-parser.js';
12
11
  export type SyqlSemanticErrorCode = 'SYQL5001_UNKNOWN_PREDICATE' | 'SYQL5002_PREDICATE_CYCLE' | 'SYQL5003_PREDICATE_ARITY' | 'SYQL5004_CLOSED_PREDICATE' | 'SYQL5005_UNUSED_PREDICATE_PARAMETER' | 'SYQL5006_UNDECLARED_BIND' | 'SYQL5007_UNUSED_INPUT' | 'SYQL5008_INVALID_CONTROL' | 'SYQL5009_MISSING_DOMINANCE' | 'SYQL5010_UNUSED_CONTROL' | 'SYQL5011_TYPE_CONFLICT';
13
12
  export interface SyqlResolvedPredicate {
14
13
  readonly id: string;
@@ -40,16 +39,12 @@ export interface SyqlLogicalPredicateNode {
40
39
  export interface SyqlLogicalWhenNode {
41
40
  readonly kind: 'when';
42
41
  readonly controls: readonly string[];
42
+ readonly explicitPresence: readonly boolean[];
43
43
  readonly controlSpans: readonly SyqlSourceSpan[];
44
44
  readonly body: readonly SyqlLogicalTemplateNode[];
45
45
  readonly span: SyqlSourceSpan;
46
46
  }
47
- export interface SyqlLogicalReactiveNode {
48
- readonly kind: 'scope' | 'cover';
49
- readonly directive: SyqlReactiveDirective;
50
- readonly span: SyqlSourceSpan;
51
- }
52
- export type SyqlLogicalTemplateNode = SyqlLogicalSqlNode | SyqlLogicalPredicateNode | SyqlLogicalWhenNode | SyqlLogicalReactiveNode;
47
+ export type SyqlLogicalTemplateNode = SyqlLogicalSqlNode | SyqlLogicalPredicateNode | SyqlLogicalWhenNode;
53
48
  export interface SyqlLogicalInput {
54
49
  readonly parameter: SyqlQueryParameter;
55
50
  /** Type declared in source or constrained by annotated predicate formals. */
@@ -70,6 +65,8 @@ export interface SyqlSemanticProgram {
70
65
  readonly predicates: readonly SyqlResolvedPredicate[];
71
66
  readonly queries: readonly SyqlLogicalQuery[];
72
67
  }
68
+ export declare function syqlRangeStartBind(name: string): string;
69
+ export declare function syqlRangeEndBind(name: string): string;
73
70
  /** Resolve and statically validate a complete reachable SYQL module graph. */
74
71
  export declare function analyzeSyqlSemantics(graph: SyqlModuleGraph): SyqlSemanticProgram;
75
72
  /** Losslessly render expanded SQL/predicate nodes, retaining `when` markers. */