@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.
@@ -6,6 +6,12 @@
6
6
  * `when` dominance, and produces a backend-neutral logical template.
7
7
  */
8
8
  import { SyqlFrontendError, } from './syql-lexer.js';
9
+ export function syqlRangeStartBind(name) {
10
+ return `__syqlRangeStart_${name}`;
11
+ }
12
+ export function syqlRangeEndBind(name) {
13
+ return `__syqlRangeEnd_${name}`;
14
+ }
9
15
  function predicateId(file, name) {
10
16
  return `${file}\0${name}`;
11
17
  }
@@ -69,7 +75,9 @@ class SemanticAnalyzer {
69
75
  for (const node of predicate.declaration.body.tree.nodes) {
70
76
  if (node.kind !== 'predicate-call')
71
77
  continue;
72
- const target = this.#resolveCall(predicate.module, node.name, node.span);
78
+ const target = this.#lookupCall(predicate.module, node.name);
79
+ if (target === undefined)
80
+ continue;
73
81
  this.#checkArity(node.arguments.length, target, node.span);
74
82
  calls.push(target);
75
83
  }
@@ -112,11 +120,14 @@ class SemanticAnalyzer {
112
120
  }
113
121
  }
114
122
  else if (node.kind === 'predicate-call') {
123
+ const target = this.#lookupCall(predicate.module, node.name);
115
124
  for (const argument of node.arguments) {
116
125
  if (!declared.has(argument.name)) {
117
126
  this.#fail('SYQL5004_CLOSED_PREDICATE', argument.span, `predicate ${predicate.declaration.name} passes undeclared bind :${argument.name}`);
118
127
  }
119
128
  }
129
+ if (target === undefined)
130
+ continue;
120
131
  }
121
132
  }
122
133
  }
@@ -135,6 +146,11 @@ class SemanticAnalyzer {
135
146
  }
136
147
  else if (node.kind === 'predicate-call') {
137
148
  const target = scope.get(node.name);
149
+ if (target === undefined) {
150
+ for (const argument of node.arguments)
151
+ used.add(argument.name);
152
+ continue;
153
+ }
138
154
  const targetUsed = computeUsed(target);
139
155
  target.declaration.parameters.forEach((formal, index) => {
140
156
  if (targetUsed.has(formal.name)) {
@@ -174,6 +190,8 @@ class SemanticAnalyzer {
174
190
  if (node.kind !== 'predicate-call')
175
191
  continue;
176
192
  const target = scope.get(node.name);
193
+ if (target === undefined)
194
+ continue;
177
195
  const targetConstraints = constraintsFor(target);
178
196
  target.declaration.parameters.forEach((formal, index) => {
179
197
  const actual = node.arguments[index];
@@ -212,8 +230,11 @@ class SemanticAnalyzer {
212
230
  }
213
231
  #analyzeQuery(module, query) {
214
232
  const bindSymbols = this.#queryBindSymbols(query);
233
+ const ranges = new Map(query.parameters.flatMap((parameter) => parameter.kind === 'range'
234
+ ? [[parameter.name, parameter]]
235
+ : []));
215
236
  const requirements = new Map();
216
- const template = this.#expandTemplate(module, query.sql.body.tree, new Map(), { queryBinds: bindSymbols, requirements });
237
+ const template = this.#expandTemplate(module, query.statement.tree, new Map(), { queryBinds: bindSymbols, ranges, requirements });
217
238
  const conditions = [];
218
239
  this.#collectConditions(template, conditions);
219
240
  this.#validateControls(query, conditions);
@@ -234,9 +255,21 @@ class SemanticAnalyzer {
234
255
  #queryBindSymbols(query) {
235
256
  const symbols = new Map();
236
257
  for (const parameter of query.parameters) {
237
- if (parameter.kind === 'switch')
238
- continue;
239
- if (parameter.kind === 'group') {
258
+ if (parameter.kind === 'range') {
259
+ for (const name of [
260
+ syqlRangeStartBind(parameter.name),
261
+ syqlRangeEndBind(parameter.name),
262
+ ]) {
263
+ symbols.set(name, {
264
+ name,
265
+ parameter,
266
+ ...(parameter.optional ? { controller: parameter.name } : {}),
267
+ ...(parameter.type === undefined ? {} : { type: parameter.type }),
268
+ span: parameter.nameSpan,
269
+ });
270
+ }
271
+ }
272
+ else if (parameter.kind === 'group') {
240
273
  for (const member of parameter.members) {
241
274
  symbols.set(member.name, {
242
275
  name: member.name,
@@ -264,48 +297,31 @@ class SemanticAnalyzer {
264
297
  }
265
298
  #expandNode(module, node, substitution, context) {
266
299
  if (node.kind === 'raw') {
267
- const parts = [];
268
- for (const token of node.tokens) {
269
- if (token.kind !== 'bind') {
270
- const previous = parts[parts.length - 1];
271
- if (previous?.kind === 'text') {
272
- parts[parts.length - 1] = {
273
- kind: 'text',
274
- text: previous.text + token.text,
275
- };
276
- }
277
- else {
278
- parts.push({ kind: 'text', text: token.text });
279
- }
280
- continue;
281
- }
282
- const authored = bindName(token);
283
- const substituted = substitution.get(authored);
284
- parts.push({
285
- kind: 'bind',
286
- name: substituted?.name ?? authored,
287
- span: token.span,
288
- origins: substituted === undefined
289
- ? [token.span]
290
- : [...substituted.origins, token.span],
291
- });
292
- }
293
- return { kind: 'sql', parts, span: node.span };
300
+ return {
301
+ kind: 'sql',
302
+ parts: this.#expandSqlTokens(node.tokens, substitution, context),
303
+ span: node.span,
304
+ };
294
305
  }
295
306
  if (node.kind === 'when') {
296
307
  return {
297
308
  kind: 'when',
298
309
  controls: node.controls,
310
+ explicitPresence: node.explicitPresence,
299
311
  controlSpans: node.controlSpans,
300
312
  body: this.#expandTemplate(module, node.body, substitution, context),
301
313
  span: node.span,
302
314
  };
303
315
  }
304
- if (node.kind === 'scope' || node.kind === 'cover') {
305
- return { kind: node.kind, directive: node, span: node.span };
306
- }
307
316
  const call = node;
308
- const target = this.#resolveCall(module, call.name, call.span);
317
+ const target = this.#lookupCall(module, call.name);
318
+ if (target === undefined) {
319
+ return {
320
+ kind: 'sql',
321
+ parts: this.#expandSqlTokens(call.tokens, substitution, context),
322
+ span: call.span,
323
+ };
324
+ }
309
325
  this.#checkArity(call.arguments.length, target, call.span);
310
326
  const targetSubstitution = new Map();
311
327
  target.declaration.parameters.forEach((formal, index) => {
@@ -330,6 +346,46 @@ class SemanticAnalyzer {
330
346
  span: call.span,
331
347
  };
332
348
  }
349
+ #expandSqlTokens(tokens, substitution, context) {
350
+ const parts = [];
351
+ const pushText = (text) => {
352
+ const previous = parts[parts.length - 1];
353
+ if (previous?.kind === 'text') {
354
+ parts[parts.length - 1] = { kind: 'text', text: previous.text + text };
355
+ }
356
+ else
357
+ parts.push({ kind: 'text', text });
358
+ };
359
+ const pushBind = (name, token, origins) => {
360
+ parts.push({ kind: 'bind', name, span: token.span, origins });
361
+ };
362
+ for (const token of tokens) {
363
+ if (token.kind !== 'bind') {
364
+ pushText(token.text);
365
+ continue;
366
+ }
367
+ const authored = bindName(token);
368
+ const substituted = substitution.get(authored);
369
+ if (substituted !== undefined) {
370
+ const parameter = context.ranges?.get(substituted.name);
371
+ if (parameter !== undefined) {
372
+ this.#fail('SYQL5011_TYPE_CONFLICT', token.span, `range input ${parameter.name} cannot be passed to a predicate`);
373
+ }
374
+ pushBind(substituted.name, token, [...substituted.origins, token.span]);
375
+ continue;
376
+ }
377
+ const range = context.ranges?.get(authored);
378
+ if (range !== undefined) {
379
+ pushBind(syqlRangeStartBind(range.name), token, [token.span]);
380
+ pushText(' and ');
381
+ pushBind(syqlRangeEndBind(range.name), token, [token.span]);
382
+ }
383
+ else {
384
+ pushBind(authored, token, [token.span]);
385
+ }
386
+ }
387
+ return parts;
388
+ }
333
389
  #collectConditions(nodes, out) {
334
390
  for (const node of nodes) {
335
391
  if (node.kind === 'when')
@@ -346,8 +402,14 @@ class SemanticAnalyzer {
346
402
  if (parameter === undefined) {
347
403
  this.#fail('SYQL5008_INVALID_CONTROL', condition.controlSpans[index], `unknown when control ${JSON.stringify(name)}`);
348
404
  }
349
- if (parameter.kind === 'value' && !parameter.optional) {
350
- this.#fail('SYQL5008_INVALID_CONTROL', condition.controlSpans[index], `required scalar ${name} cannot control when; only optional values, groups, and switches can`);
405
+ if (condition.explicitPresence[index] && !parameter.optional) {
406
+ this.#fail('SYQL5008_INVALID_CONTROL', condition.controlSpans[index], `present(${name}) requires an optional input`);
407
+ }
408
+ if (!parameter.optional &&
409
+ !(parameter.kind === 'value' &&
410
+ parameter.type?.base === 'boolean' &&
411
+ parameter.default === false)) {
412
+ this.#fail('SYQL5008_INVALID_CONTROL', condition.controlSpans[index], `required input ${name} cannot control when; use an optional input or a bool defaulted to false`);
351
413
  }
352
414
  });
353
415
  }
@@ -392,18 +454,6 @@ class SemanticAnalyzer {
392
454
  }
393
455
  walk(node.body, next);
394
456
  }
395
- else {
396
- for (const binding of node.directive.bindings) {
397
- for (const value of binding.values) {
398
- record({
399
- kind: 'bind',
400
- name: value.name,
401
- span: value.span,
402
- origins: [value.span],
403
- }, active);
404
- }
405
- }
406
- }
407
457
  }
408
458
  };
409
459
  walk(template, new Set());
@@ -424,18 +474,29 @@ class SemanticAnalyzer {
424
474
  collect(condition.body);
425
475
  condition.controls.forEach((control, index) => {
426
476
  const parameter = query.parameters.find((item) => item.name === control);
427
- if (parameter?.kind === 'value' && !conditionUses.has(control)) {
477
+ if (parameter?.kind === 'value' &&
478
+ parameter.default === undefined &&
479
+ !conditionUses.has(control)) {
428
480
  this.#fail('SYQL5010_UNUSED_CONTROL', condition.controlSpans[index], `when(${control}) does not use :${control} in its body`);
429
481
  }
430
482
  if (parameter?.kind === 'group' &&
431
483
  !parameter.members.some((member) => conditionUses.has(member.name))) {
432
484
  this.#fail('SYQL5010_UNUSED_CONTROL', condition.controlSpans[index], `when(${control}) does not use a member of group ${control}`);
433
485
  }
486
+ if (parameter?.kind === 'range' &&
487
+ !conditionUses.has(syqlRangeStartBind(parameter.name))) {
488
+ this.#fail('SYQL5010_UNUSED_CONTROL', condition.controlSpans[index], `when(${control}) does not use range :${control}`);
489
+ }
434
490
  });
435
491
  }
436
492
  for (const parameter of query.parameters) {
437
- if (parameter.kind === 'switch') {
438
- if (!usedControls.has(parameter.name)) {
493
+ if (parameter.kind === 'range') {
494
+ if (parameter.optional && !usedControls.has(parameter.name)) {
495
+ this.#unusedInput(parameter.nameSpan, parameter.name);
496
+ }
497
+ const start = syqlRangeStartBind(parameter.name);
498
+ const end = syqlRangeEndBind(parameter.name);
499
+ if (!allUses.has(start) || !allUses.has(end)) {
439
500
  this.#unusedInput(parameter.nameSpan, parameter.name);
440
501
  }
441
502
  }
@@ -454,7 +515,8 @@ class SemanticAnalyzer {
454
515
  this.#unusedInput(parameter.nameSpan, parameter.name);
455
516
  }
456
517
  }
457
- else if (!allUses.has(parameter.name)) {
518
+ else if (!allUses.has(parameter.name) &&
519
+ !(parameter.default !== undefined && usedControls.has(parameter.name))) {
458
520
  this.#unusedInput(parameter.nameSpan, parameter.name);
459
521
  }
460
522
  }
@@ -497,26 +559,20 @@ class SemanticAnalyzer {
497
559
  return resolved;
498
560
  }
499
561
  #parameterType(parameter, resolved) {
500
- if (parameter.kind === 'switch')
501
- return undefined;
502
- if (parameter.kind === 'group') {
562
+ if (parameter.kind === 'range' || parameter.kind === 'group') {
503
563
  // Group members carry their own types; the logical input itself has no
504
564
  // scalar type. L3 consumes the per-member resolved map directly.
505
565
  return undefined;
506
566
  }
507
567
  return parameter.type ?? resolved.get(parameter.name);
508
568
  }
509
- #resolveCall(module, name, span) {
510
- const target = this.#scopes.get(module.file)?.get(name);
511
- if (target === undefined) {
512
- this.#fail('SYQL5001_UNKNOWN_PREDICATE', span, `unknown predicate @${name} in ${module.file}`);
513
- }
514
- return target;
569
+ #lookupCall(module, name) {
570
+ return this.#scopes.get(module.file)?.get(name);
515
571
  }
516
572
  #checkArity(actual, target, span) {
517
573
  const expected = target.declaration.parameters.length;
518
574
  if (actual !== expected) {
519
- this.#fail('SYQL5003_PREDICATE_ARITY', span, `@${target.declaration.name} expects ${expected} argument(s), got ${actual}`);
575
+ this.#fail('SYQL5003_PREDICATE_ARITY', span, `${target.declaration.name} expects ${expected} argument(s), got ${actual}`);
520
576
  }
521
577
  }
522
578
  #unusedInput(span, name) {
@@ -545,7 +601,7 @@ export function renderSyqlLogicalTemplate(nodes) {
545
601
  if (node.kind === 'when') {
546
602
  return `when(${node.controls.join(', ')}) {${renderSyqlLogicalTemplate(node.body)}}`;
547
603
  }
548
- return node.directive.tokens.map((token) => token.text).join('');
604
+ throw new Error('unknown SYQL logical template node');
549
605
  })
550
606
  .join('');
551
607
  }
@@ -23,30 +23,15 @@ export interface SyqlPredicateCall {
23
23
  export interface SyqlWhenExpression {
24
24
  readonly kind: 'when';
25
25
  readonly controls: readonly string[];
26
+ /** True when the author wrote present(control); bare optional controls have
27
+ * the same semantics and therefore normally keep this false. */
28
+ readonly explicitPresence: readonly boolean[];
26
29
  readonly controlSpans: readonly SyqlSourceSpan[];
27
30
  readonly body: SyqlEmbeddedTemplate;
28
31
  readonly tokens: readonly SyqlToken[];
29
32
  readonly span: SyqlSourceSpan;
30
33
  }
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;
34
+ export type SyqlEmbeddedNode = SyqlRawTemplateNode | SyqlPredicateCall | SyqlWhenExpression;
50
35
  export interface SyqlEmbeddedTemplate {
51
36
  readonly kind: 'template';
52
37
  readonly mode: SyqlTemplateMode;
@@ -55,4 +40,4 @@ export interface SyqlEmbeddedTemplate {
55
40
  }
56
41
  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
42
  /** Parse embedded nodes from one already-delimited lossless template. */
58
- export declare function parseSyqlEmbeddedTemplate(file: string, tokens: readonly SyqlToken[], span: SyqlSourceSpan, mode: SyqlTemplateMode): SyqlEmbeddedTemplate;
43
+ export declare function parseSyqlEmbeddedTemplate(file: string, tokens: readonly SyqlToken[], span: SyqlSourceSpan, mode: SyqlTemplateMode, rangeNames?: ReadonlySet<string>): SyqlEmbeddedTemplate;