@syncular/typegen 0.6.0 → 0.7.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.
@@ -15,6 +15,7 @@ import type {
15
15
  SyqlPredicateDeclaration,
16
16
  SyqlQueryDeclaration,
17
17
  SyqlQueryParameter,
18
+ SyqlRangeParameter,
18
19
  SyqlSyntaxFile,
19
20
  SyqlValueType,
20
21
  } from './syql-parser';
@@ -22,7 +23,6 @@ import type {
22
23
  SyqlEmbeddedNode,
23
24
  SyqlEmbeddedTemplate,
24
25
  SyqlPredicateCall,
25
- SyqlReactiveDirective,
26
26
  } from './syql-template-parser';
27
27
 
28
28
  export type SyqlSemanticErrorCode =
@@ -72,22 +72,16 @@ export interface SyqlLogicalPredicateNode {
72
72
  export interface SyqlLogicalWhenNode {
73
73
  readonly kind: 'when';
74
74
  readonly controls: readonly string[];
75
+ readonly explicitPresence: readonly boolean[];
75
76
  readonly controlSpans: readonly SyqlSourceSpan[];
76
77
  readonly body: readonly SyqlLogicalTemplateNode[];
77
78
  readonly span: SyqlSourceSpan;
78
79
  }
79
80
 
80
- export interface SyqlLogicalReactiveNode {
81
- readonly kind: 'scope' | 'cover';
82
- readonly directive: SyqlReactiveDirective;
83
- readonly span: SyqlSourceSpan;
84
- }
85
-
86
81
  export type SyqlLogicalTemplateNode =
87
82
  | SyqlLogicalSqlNode
88
83
  | SyqlLogicalPredicateNode
89
- | SyqlLogicalWhenNode
90
- | SyqlLogicalReactiveNode;
84
+ | SyqlLogicalWhenNode;
91
85
 
92
86
  export interface SyqlLogicalInput {
93
87
  readonly parameter: SyqlQueryParameter;
@@ -131,9 +125,18 @@ interface QueryBindSymbol {
131
125
 
132
126
  interface ExpansionContext {
133
127
  readonly queryBinds?: ReadonlyMap<string, QueryBindSymbol>;
128
+ readonly ranges?: ReadonlyMap<string, SyqlRangeParameter>;
134
129
  readonly requirements?: Map<string, SyqlValueType[]>;
135
130
  }
136
131
 
132
+ export function syqlRangeStartBind(name: string): string {
133
+ return `__syqlRangeStart_${name}`;
134
+ }
135
+
136
+ export function syqlRangeEndBind(name: string): string {
137
+ return `__syqlRangeEnd_${name}`;
138
+ }
139
+
137
140
  function predicateId(file: string, name: string): string {
138
141
  return `${file}\0${name}`;
139
142
  }
@@ -213,11 +216,8 @@ class SemanticAnalyzer {
213
216
  const calls: SyqlResolvedPredicate[] = [];
214
217
  for (const node of predicate.declaration.body.tree.nodes) {
215
218
  if (node.kind !== 'predicate-call') continue;
216
- const target = this.#resolveCall(
217
- predicate.module,
218
- node.name,
219
- node.span,
220
- );
219
+ const target = this.#lookupCall(predicate.module, node.name);
220
+ if (target === undefined) continue;
221
221
  this.#checkArity(node.arguments.length, target, node.span);
222
222
  calls.push(target);
223
223
  }
@@ -270,6 +270,7 @@ class SemanticAnalyzer {
270
270
  }
271
271
  }
272
272
  } else if (node.kind === 'predicate-call') {
273
+ const target = this.#lookupCall(predicate.module, node.name);
273
274
  for (const argument of node.arguments) {
274
275
  if (!declared.has(argument.name)) {
275
276
  this.#fail(
@@ -279,6 +280,7 @@ class SemanticAnalyzer {
279
280
  );
280
281
  }
281
282
  }
283
+ if (target === undefined) continue;
282
284
  }
283
285
  }
284
286
  }
@@ -299,7 +301,11 @@ class SemanticAnalyzer {
299
301
  if (token.kind === 'bind') used.add(bindName(token));
300
302
  }
301
303
  } else if (node.kind === 'predicate-call') {
302
- const target = scope.get(node.name) as SyqlResolvedPredicate;
304
+ const target = scope.get(node.name);
305
+ if (target === undefined) {
306
+ for (const argument of node.arguments) used.add(argument.name);
307
+ continue;
308
+ }
303
309
  const targetUsed = computeUsed(target);
304
310
  target.declaration.parameters.forEach((formal, index) => {
305
311
  if (targetUsed.has(formal.name)) {
@@ -350,7 +356,8 @@ class SemanticAnalyzer {
350
356
  >;
351
357
  for (const node of predicate.declaration.body.tree.nodes) {
352
358
  if (node.kind !== 'predicate-call') continue;
353
- const target = scope.get(node.name) as SyqlResolvedPredicate;
359
+ const target = scope.get(node.name);
360
+ if (target === undefined) continue;
354
361
  const targetConstraints = constraintsFor(target);
355
362
  target.declaration.parameters.forEach((formal, index) => {
356
363
  const actual = node.arguments[index];
@@ -402,12 +409,19 @@ class SemanticAnalyzer {
402
409
  query: SyqlQueryDeclaration,
403
410
  ): SyqlLogicalQuery {
404
411
  const bindSymbols = this.#queryBindSymbols(query);
412
+ const ranges = new Map(
413
+ query.parameters.flatMap((parameter) =>
414
+ parameter.kind === 'range'
415
+ ? [[parameter.name, parameter] as const]
416
+ : [],
417
+ ),
418
+ );
405
419
  const requirements = new Map<string, SyqlValueType[]>();
406
420
  const template = this.#expandTemplate(
407
421
  module,
408
- query.sql.body.tree,
422
+ query.statement.tree,
409
423
  new Map(),
410
- { queryBinds: bindSymbols, requirements },
424
+ { queryBinds: bindSymbols, ranges, requirements },
411
425
  );
412
426
  const conditions: SyqlLogicalWhenNode[] = [];
413
427
  this.#collectConditions(template, conditions);
@@ -435,8 +449,20 @@ class SemanticAnalyzer {
435
449
  ): ReadonlyMap<string, QueryBindSymbol> {
436
450
  const symbols = new Map<string, QueryBindSymbol>();
437
451
  for (const parameter of query.parameters) {
438
- if (parameter.kind === 'switch') continue;
439
- if (parameter.kind === 'group') {
452
+ if (parameter.kind === 'range') {
453
+ for (const name of [
454
+ syqlRangeStartBind(parameter.name),
455
+ syqlRangeEndBind(parameter.name),
456
+ ]) {
457
+ symbols.set(name, {
458
+ name,
459
+ parameter,
460
+ ...(parameter.optional ? { controller: parameter.name } : {}),
461
+ ...(parameter.type === undefined ? {} : { type: parameter.type }),
462
+ span: parameter.nameSpan,
463
+ });
464
+ }
465
+ } else if (parameter.kind === 'group') {
440
466
  for (const member of parameter.members) {
441
467
  symbols.set(member.name, {
442
468
  name: member.name,
@@ -477,48 +503,31 @@ class SemanticAnalyzer {
477
503
  context: ExpansionContext,
478
504
  ): SyqlLogicalTemplateNode {
479
505
  if (node.kind === 'raw') {
480
- const parts: SyqlSqlPart[] = [];
481
- for (const token of node.tokens) {
482
- if (token.kind !== 'bind') {
483
- const previous = parts[parts.length - 1];
484
- if (previous?.kind === 'text') {
485
- parts[parts.length - 1] = {
486
- kind: 'text',
487
- text: previous.text + token.text,
488
- };
489
- } else {
490
- parts.push({ kind: 'text', text: token.text });
491
- }
492
- continue;
493
- }
494
- const authored = bindName(token);
495
- const substituted = substitution.get(authored);
496
- parts.push({
497
- kind: 'bind',
498
- name: substituted?.name ?? authored,
499
- span: token.span,
500
- origins:
501
- substituted === undefined
502
- ? [token.span]
503
- : [...substituted.origins, token.span],
504
- });
505
- }
506
- return { kind: 'sql', parts, span: node.span };
506
+ return {
507
+ kind: 'sql',
508
+ parts: this.#expandSqlTokens(node.tokens, substitution, context),
509
+ span: node.span,
510
+ };
507
511
  }
508
512
  if (node.kind === 'when') {
509
513
  return {
510
514
  kind: 'when',
511
515
  controls: node.controls,
516
+ explicitPresence: node.explicitPresence,
512
517
  controlSpans: node.controlSpans,
513
518
  body: this.#expandTemplate(module, node.body, substitution, context),
514
519
  span: node.span,
515
520
  };
516
521
  }
517
- if (node.kind === 'scope' || node.kind === 'cover') {
518
- return { kind: node.kind, directive: node, span: node.span };
519
- }
520
522
  const call = node as SyqlPredicateCall;
521
- const target = this.#resolveCall(module, call.name, call.span);
523
+ const target = this.#lookupCall(module, call.name);
524
+ if (target === undefined) {
525
+ return {
526
+ kind: 'sql',
527
+ parts: this.#expandSqlTokens(call.tokens, substitution, context),
528
+ span: call.span,
529
+ };
530
+ }
522
531
  this.#checkArity(call.arguments.length, target, call.span);
523
532
  const targetSubstitution = new Map<string, ActualBind>();
524
533
  target.declaration.parameters.forEach((formal, index) => {
@@ -549,6 +558,56 @@ class SemanticAnalyzer {
549
558
  };
550
559
  }
551
560
 
561
+ #expandSqlTokens(
562
+ tokens: readonly SyqlToken[],
563
+ substitution: ReadonlyMap<string, ActualBind>,
564
+ context: ExpansionContext,
565
+ ): readonly SyqlSqlPart[] {
566
+ const parts: SyqlSqlPart[] = [];
567
+ const pushText = (text: string): void => {
568
+ const previous = parts[parts.length - 1];
569
+ if (previous?.kind === 'text') {
570
+ parts[parts.length - 1] = { kind: 'text', text: previous.text + text };
571
+ } else parts.push({ kind: 'text', text });
572
+ };
573
+ const pushBind = (
574
+ name: string,
575
+ token: SyqlToken,
576
+ origins: readonly SyqlSourceSpan[],
577
+ ): void => {
578
+ parts.push({ kind: 'bind', name, span: token.span, origins });
579
+ };
580
+ for (const token of tokens) {
581
+ if (token.kind !== 'bind') {
582
+ pushText(token.text);
583
+ continue;
584
+ }
585
+ const authored = bindName(token);
586
+ const substituted = substitution.get(authored);
587
+ if (substituted !== undefined) {
588
+ const parameter = context.ranges?.get(substituted.name);
589
+ if (parameter !== undefined) {
590
+ this.#fail(
591
+ 'SYQL5011_TYPE_CONFLICT',
592
+ token.span,
593
+ `range input ${parameter.name} cannot be passed to a predicate`,
594
+ );
595
+ }
596
+ pushBind(substituted.name, token, [...substituted.origins, token.span]);
597
+ continue;
598
+ }
599
+ const range = context.ranges?.get(authored);
600
+ if (range !== undefined) {
601
+ pushBind(syqlRangeStartBind(range.name), token, [token.span]);
602
+ pushText(' and ');
603
+ pushBind(syqlRangeEndBind(range.name), token, [token.span]);
604
+ } else {
605
+ pushBind(authored, token, [token.span]);
606
+ }
607
+ }
608
+ return parts;
609
+ }
610
+
552
611
  #collectConditions(
553
612
  nodes: readonly SyqlLogicalTemplateNode[],
554
613
  out: SyqlLogicalWhenNode[],
@@ -575,11 +634,25 @@ class SemanticAnalyzer {
575
634
  `unknown when control ${JSON.stringify(name)}`,
576
635
  );
577
636
  }
578
- if (parameter.kind === 'value' && !parameter.optional) {
637
+ if (condition.explicitPresence[index] && !parameter.optional) {
638
+ this.#fail(
639
+ 'SYQL5008_INVALID_CONTROL',
640
+ condition.controlSpans[index] as SyqlSourceSpan,
641
+ `present(${name}) requires an optional input`,
642
+ );
643
+ }
644
+ if (
645
+ !parameter.optional &&
646
+ !(
647
+ parameter.kind === 'value' &&
648
+ parameter.type?.base === 'boolean' &&
649
+ parameter.default === false
650
+ )
651
+ ) {
579
652
  this.#fail(
580
653
  'SYQL5008_INVALID_CONTROL',
581
654
  condition.controlSpans[index] as SyqlSourceSpan,
582
- `required scalar ${name} cannot control when; only optional values, groups, and switches can`,
655
+ `required input ${name} cannot control when; use an optional input or a bool defaulted to false`,
583
656
  );
584
657
  }
585
658
  });
@@ -644,20 +717,6 @@ class SemanticAnalyzer {
644
717
  next.add(control);
645
718
  }
646
719
  walk(node.body, next);
647
- } else {
648
- for (const binding of node.directive.bindings) {
649
- for (const value of binding.values) {
650
- record(
651
- {
652
- kind: 'bind',
653
- name: value.name,
654
- span: value.span,
655
- origins: [value.span],
656
- },
657
- active,
658
- );
659
- }
660
- }
661
720
  }
662
721
  }
663
722
  };
@@ -679,7 +738,11 @@ class SemanticAnalyzer {
679
738
  const parameter = query.parameters.find(
680
739
  (item) => item.name === control,
681
740
  );
682
- if (parameter?.kind === 'value' && !conditionUses.has(control)) {
741
+ if (
742
+ parameter?.kind === 'value' &&
743
+ parameter.default === undefined &&
744
+ !conditionUses.has(control)
745
+ ) {
683
746
  this.#fail(
684
747
  'SYQL5010_UNUSED_CONTROL',
685
748
  condition.controlSpans[index] as SyqlSourceSpan,
@@ -696,12 +759,27 @@ class SemanticAnalyzer {
696
759
  `when(${control}) does not use a member of group ${control}`,
697
760
  );
698
761
  }
762
+ if (
763
+ parameter?.kind === 'range' &&
764
+ !conditionUses.has(syqlRangeStartBind(parameter.name))
765
+ ) {
766
+ this.#fail(
767
+ 'SYQL5010_UNUSED_CONTROL',
768
+ condition.controlSpans[index] as SyqlSourceSpan,
769
+ `when(${control}) does not use range :${control}`,
770
+ );
771
+ }
699
772
  });
700
773
  }
701
774
 
702
775
  for (const parameter of query.parameters) {
703
- if (parameter.kind === 'switch') {
704
- if (!usedControls.has(parameter.name)) {
776
+ if (parameter.kind === 'range') {
777
+ if (parameter.optional && !usedControls.has(parameter.name)) {
778
+ this.#unusedInput(parameter.nameSpan, parameter.name);
779
+ }
780
+ const start = syqlRangeStartBind(parameter.name);
781
+ const end = syqlRangeEndBind(parameter.name);
782
+ if (!allUses.has(start) || !allUses.has(end)) {
705
783
  this.#unusedInput(parameter.nameSpan, parameter.name);
706
784
  }
707
785
  } else if (parameter.kind === 'group') {
@@ -717,7 +795,10 @@ class SemanticAnalyzer {
717
795
  if (!usedControls.has(parameter.name) || !allUses.has(parameter.name)) {
718
796
  this.#unusedInput(parameter.nameSpan, parameter.name);
719
797
  }
720
- } else if (!allUses.has(parameter.name)) {
798
+ } else if (
799
+ !allUses.has(parameter.name) &&
800
+ !(parameter.default !== undefined && usedControls.has(parameter.name))
801
+ ) {
721
802
  this.#unusedInput(parameter.nameSpan, parameter.name);
722
803
  }
723
804
  }
@@ -772,8 +853,7 @@ class SemanticAnalyzer {
772
853
  parameter: SyqlQueryParameter,
773
854
  resolved: ReadonlyMap<string, SyqlValueType>,
774
855
  ): SyqlValueType | undefined {
775
- if (parameter.kind === 'switch') return undefined;
776
- if (parameter.kind === 'group') {
856
+ if (parameter.kind === 'range' || parameter.kind === 'group') {
777
857
  // Group members carry their own types; the logical input itself has no
778
858
  // scalar type. L3 consumes the per-member resolved map directly.
779
859
  return undefined;
@@ -781,20 +861,11 @@ class SemanticAnalyzer {
781
861
  return parameter.type ?? resolved.get(parameter.name);
782
862
  }
783
863
 
784
- #resolveCall(
864
+ #lookupCall(
785
865
  module: SyqlSyntaxFile,
786
866
  name: string,
787
- span: SyqlSourceSpan,
788
- ): SyqlResolvedPredicate {
789
- const target = this.#scopes.get(module.file)?.get(name);
790
- if (target === undefined) {
791
- this.#fail(
792
- 'SYQL5001_UNKNOWN_PREDICATE',
793
- span,
794
- `unknown predicate @${name} in ${module.file}`,
795
- );
796
- }
797
- return target;
867
+ ): SyqlResolvedPredicate | undefined {
868
+ return this.#scopes.get(module.file)?.get(name);
798
869
  }
799
870
 
800
871
  #checkArity(
@@ -807,7 +878,7 @@ class SemanticAnalyzer {
807
878
  this.#fail(
808
879
  'SYQL5003_PREDICATE_ARITY',
809
880
  span,
810
- `@${target.declaration.name} expects ${expected} argument(s), got ${actual}`,
881
+ `${target.declaration.name} expects ${expected} argument(s), got ${actual}`,
811
882
  );
812
883
  }
813
884
  }
@@ -853,7 +924,7 @@ export function renderSyqlLogicalTemplate(
853
924
  if (node.kind === 'when') {
854
925
  return `when(${node.controls.join(', ')}) {${renderSyqlLogicalTemplate(node.body)}}`;
855
926
  }
856
- return node.directive.tokens.map((token) => token.text).join('');
927
+ throw new Error('unknown SYQL logical template node');
857
928
  })
858
929
  .join('');
859
930
  }