@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.
- package/README.md +47 -42
- package/dist/cli.js +1 -4
- package/dist/emit-queries-dart.js +9 -8
- package/dist/emit-queries-kotlin.js +9 -8
- package/dist/emit-queries-swift.js +9 -8
- package/dist/emit-queries.js +10 -13
- package/dist/fmt.js +29 -4
- package/dist/lsp.js +16 -28
- package/dist/query.d.ts +6 -11
- package/dist/query.js +29 -0
- package/dist/syql-ast.d.ts +12 -13
- package/dist/syql-ast.js +26 -20
- package/dist/syql-lowering.js +65 -42
- package/dist/syql-parser.d.ts +19 -18
- package/dist/syql-parser.js +341 -93
- package/dist/syql-semantics.d.ts +4 -7
- package/dist/syql-semantics.js +121 -65
- package/dist/syql-template-parser.d.ts +5 -20
- package/dist/syql-template-parser.js +116 -109
- package/dist/syql-validator.d.ts +2 -2
- package/dist/syql-validator.js +251 -224
- package/package.json +3 -3
- package/src/cli.ts +1 -3
- package/src/emit-queries-dart.ts +7 -7
- package/src/emit-queries-kotlin.ts +7 -7
- package/src/emit-queries-swift.ts +7 -7
- package/src/emit-queries.ts +9 -12
- package/src/fmt.ts +32 -4
- package/src/lsp.ts +16 -28
- package/src/query.ts +42 -12
- package/src/syql-ast.ts +38 -34
- package/src/syql-lowering.ts +69 -43
- package/src/syql-parser.ts +472 -134
- package/src/syql-semantics.ts +158 -87
- package/src/syql-template-parser.ts +119 -163
- package/src/syql-validator.ts +330 -336
package/dist/syql-semantics.js
CHANGED
|
@@ -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.#
|
|
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.
|
|
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 === '
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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.#
|
|
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 (
|
|
350
|
-
this.#fail('SYQL5008_INVALID_CONTROL', condition.controlSpans[index], `
|
|
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' &&
|
|
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 === '
|
|
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 === '
|
|
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
|
-
#
|
|
510
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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;
|