@syncular/typegen 0.5.1 → 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.
- package/README.md +79 -32
- package/dist/cli.js +40 -17
- package/dist/emit-queries-dart.js +168 -55
- package/dist/emit-queries-kotlin.js +167 -55
- package/dist/emit-queries-swift.js +183 -57
- package/dist/emit-queries.js +286 -123
- package/dist/fmt.d.ts +2 -2
- package/dist/fmt.js +348 -316
- package/dist/generate.d.ts +1 -1
- package/dist/generate.js +28 -9
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/lsp.d.ts +1 -3
- package/dist/lsp.js +349 -187
- package/dist/manifest.d.ts +1 -3
- package/dist/manifest.js +1 -1
- package/dist/query-ir.js +53 -42
- package/dist/query.d.ts +110 -63
- package/dist/query.js +89 -11
- package/dist/syql-ast.d.ts +12 -13
- package/dist/syql-ast.js +26 -20
- package/dist/syql-lexer.d.ts +2 -0
- package/dist/syql-lexer.js +9 -2
- package/dist/syql-lowering.d.ts +22 -0
- package/dist/syql-lowering.js +434 -0
- package/dist/syql-parser.d.ts +19 -18
- package/dist/syql-parser.js +341 -93
- package/dist/syql-semantics.d.ts +73 -0
- package/dist/syql-semantics.js +607 -0
- package/dist/syql-template-parser.d.ts +5 -20
- package/dist/syql-template-parser.js +116 -109
- package/dist/syql-validator.d.ts +35 -0
- package/dist/syql-validator.js +993 -0
- package/package.json +3 -3
- package/src/cli.ts +49 -21
- package/src/emit-queries-dart.ts +195 -69
- package/src/emit-queries-kotlin.ts +197 -69
- package/src/emit-queries-swift.ts +224 -68
- package/src/emit-queries.ts +410 -165
- package/src/fmt.ts +405 -303
- package/src/generate.ts +43 -9
- package/src/index.ts +3 -1
- package/src/lsp.ts +414 -216
- package/src/manifest.ts +2 -4
- package/src/query-ir.ts +52 -42
- package/src/query.ts +229 -79
- package/src/syql-ast.ts +38 -34
- package/src/syql-lexer.ts +12 -1
- package/src/syql-lowering.ts +664 -0
- package/src/syql-parser.ts +472 -134
- package/src/syql-semantics.ts +930 -0
- package/src/syql-template-parser.ts +119 -163
- package/src/syql-validator.ts +1486 -0
- package/dist/syql.d.ts +0 -102
- package/dist/syql.js +0 -1141
- package/src/syql.ts +0 -1470
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revision-1 SYQL module/name/signature semantics (§§4–8).
|
|
3
|
+
*
|
|
4
|
+
* This pass deliberately runs before SQLite/schema analysis. It resolves and
|
|
5
|
+
* expands predicates using token nodes, proves authoritative query inputs and
|
|
6
|
+
* `when` dominance, and produces a backend-neutral logical template.
|
|
7
|
+
*/
|
|
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
|
+
}
|
|
15
|
+
function predicateId(file, name) {
|
|
16
|
+
return `${file}\0${name}`;
|
|
17
|
+
}
|
|
18
|
+
function bindName(token) {
|
|
19
|
+
return token.text.slice(1);
|
|
20
|
+
}
|
|
21
|
+
function typeText(type) {
|
|
22
|
+
return `${type.base}${type.nullable ? ' | null' : ''}`;
|
|
23
|
+
}
|
|
24
|
+
function isCompatible(actual, formal) {
|
|
25
|
+
return actual.base === formal.base && (!actual.nullable || formal.nullable);
|
|
26
|
+
}
|
|
27
|
+
class SemanticAnalyzer {
|
|
28
|
+
#graph;
|
|
29
|
+
#predicateById = new Map();
|
|
30
|
+
#scopes = new Map();
|
|
31
|
+
#calls = new Map();
|
|
32
|
+
#usedPredicateParams = new Map();
|
|
33
|
+
constructor(graph) {
|
|
34
|
+
this.#graph = graph;
|
|
35
|
+
}
|
|
36
|
+
analyze() {
|
|
37
|
+
this.#buildScopes();
|
|
38
|
+
this.#resolvePredicateCalls();
|
|
39
|
+
this.#checkPredicateCycles();
|
|
40
|
+
this.#checkPredicateTypes();
|
|
41
|
+
this.#checkPredicateClosureAndUse();
|
|
42
|
+
const queries = this.#graph.modules.flatMap((module) => module.queries.map((query) => this.#analyzeQuery(module, query)));
|
|
43
|
+
return {
|
|
44
|
+
graph: this.#graph,
|
|
45
|
+
predicateScopes: this.#scopes,
|
|
46
|
+
predicates: [...this.#predicateById.values()],
|
|
47
|
+
queries,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
#buildScopes() {
|
|
51
|
+
for (const module of this.#graph.modules) {
|
|
52
|
+
const scope = new Map();
|
|
53
|
+
for (const declaration of module.predicates) {
|
|
54
|
+
const resolved = {
|
|
55
|
+
id: predicateId(module.file, declaration.name),
|
|
56
|
+
module,
|
|
57
|
+
declaration,
|
|
58
|
+
};
|
|
59
|
+
this.#predicateById.set(resolved.id, resolved);
|
|
60
|
+
scope.set(declaration.name, resolved);
|
|
61
|
+
}
|
|
62
|
+
this.#scopes.set(module.file, scope);
|
|
63
|
+
}
|
|
64
|
+
for (const edge of this.#graph.edges) {
|
|
65
|
+
const scope = this.#scopes.get(edge.from);
|
|
66
|
+
for (const item of edge.declaration.items) {
|
|
67
|
+
const target = this.#predicateById.get(predicateId(edge.to, item.imported));
|
|
68
|
+
scope.set(item.local, target);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
#resolvePredicateCalls() {
|
|
73
|
+
for (const predicate of this.#predicateById.values()) {
|
|
74
|
+
const calls = [];
|
|
75
|
+
for (const node of predicate.declaration.body.tree.nodes) {
|
|
76
|
+
if (node.kind !== 'predicate-call')
|
|
77
|
+
continue;
|
|
78
|
+
const target = this.#lookupCall(predicate.module, node.name);
|
|
79
|
+
if (target === undefined)
|
|
80
|
+
continue;
|
|
81
|
+
this.#checkArity(node.arguments.length, target, node.span);
|
|
82
|
+
calls.push(target);
|
|
83
|
+
}
|
|
84
|
+
this.#calls.set(predicate.id, calls);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
#checkPredicateCycles() {
|
|
88
|
+
const states = new Map();
|
|
89
|
+
const stack = [];
|
|
90
|
+
const visit = (predicate) => {
|
|
91
|
+
const state = states.get(predicate.id);
|
|
92
|
+
if (state === 'complete')
|
|
93
|
+
return;
|
|
94
|
+
if (state === 'active') {
|
|
95
|
+
const start = stack.findIndex((item) => item.id === predicate.id);
|
|
96
|
+
const cycle = [...stack.slice(start), predicate]
|
|
97
|
+
.map((item) => `${item.declaration.name} (${item.module.file})`)
|
|
98
|
+
.join(' -> ');
|
|
99
|
+
this.#fail('SYQL5002_PREDICATE_CYCLE', predicate.declaration.nameSpan, `predicate call cycle: ${cycle}`);
|
|
100
|
+
}
|
|
101
|
+
states.set(predicate.id, 'active');
|
|
102
|
+
stack.push(predicate);
|
|
103
|
+
for (const target of this.#calls.get(predicate.id) ?? [])
|
|
104
|
+
visit(target);
|
|
105
|
+
stack.pop();
|
|
106
|
+
states.set(predicate.id, 'complete');
|
|
107
|
+
};
|
|
108
|
+
for (const predicate of this.#predicateById.values())
|
|
109
|
+
visit(predicate);
|
|
110
|
+
}
|
|
111
|
+
#checkPredicateClosureAndUse() {
|
|
112
|
+
for (const predicate of this.#predicateById.values()) {
|
|
113
|
+
const declared = new Set(predicate.declaration.parameters.map((parameter) => parameter.name));
|
|
114
|
+
for (const node of predicate.declaration.body.tree.nodes) {
|
|
115
|
+
if (node.kind === 'raw') {
|
|
116
|
+
for (const token of node.tokens) {
|
|
117
|
+
if (token.kind === 'bind' && !declared.has(bindName(token))) {
|
|
118
|
+
this.#fail('SYQL5004_CLOSED_PREDICATE', token.span, `predicate ${predicate.declaration.name} uses undeclared bind ${token.text}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else if (node.kind === 'predicate-call') {
|
|
123
|
+
const target = this.#lookupCall(predicate.module, node.name);
|
|
124
|
+
for (const argument of node.arguments) {
|
|
125
|
+
if (!declared.has(argument.name)) {
|
|
126
|
+
this.#fail('SYQL5004_CLOSED_PREDICATE', argument.span, `predicate ${predicate.declaration.name} passes undeclared bind :${argument.name}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (target === undefined)
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const computeUsed = (predicate) => {
|
|
135
|
+
const cached = this.#usedPredicateParams.get(predicate.id);
|
|
136
|
+
if (cached !== undefined)
|
|
137
|
+
return cached;
|
|
138
|
+
const used = new Set();
|
|
139
|
+
const scope = this.#scopes.get(predicate.module.file);
|
|
140
|
+
for (const node of predicate.declaration.body.tree.nodes) {
|
|
141
|
+
if (node.kind === 'raw') {
|
|
142
|
+
for (const token of node.tokens) {
|
|
143
|
+
if (token.kind === 'bind')
|
|
144
|
+
used.add(bindName(token));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else if (node.kind === 'predicate-call') {
|
|
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
|
+
}
|
|
154
|
+
const targetUsed = computeUsed(target);
|
|
155
|
+
target.declaration.parameters.forEach((formal, index) => {
|
|
156
|
+
if (targetUsed.has(formal.name)) {
|
|
157
|
+
const actual = node.arguments[index];
|
|
158
|
+
if (actual !== undefined)
|
|
159
|
+
used.add(actual.name);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
this.#usedPredicateParams.set(predicate.id, used);
|
|
165
|
+
return used;
|
|
166
|
+
};
|
|
167
|
+
for (const predicate of this.#predicateById.values()) {
|
|
168
|
+
const used = computeUsed(predicate);
|
|
169
|
+
for (const parameter of predicate.declaration.parameters) {
|
|
170
|
+
if (!used.has(parameter.name)) {
|
|
171
|
+
this.#fail('SYQL5005_UNUSED_PREDICATE_PARAMETER', parameter.nameSpan, `predicate parameter ${parameter.name} is unused after expansion`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
#checkPredicateTypes() {
|
|
177
|
+
const cache = new Map();
|
|
178
|
+
const constraintsFor = (predicate) => {
|
|
179
|
+
const cached = cache.get(predicate.id);
|
|
180
|
+
if (cached !== undefined)
|
|
181
|
+
return cached;
|
|
182
|
+
const constraints = new Map();
|
|
183
|
+
for (const parameter of predicate.declaration.parameters) {
|
|
184
|
+
if (parameter.type !== undefined) {
|
|
185
|
+
constraints.set(parameter.name, [parameter.type]);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const scope = this.#scopes.get(predicate.module.file);
|
|
189
|
+
for (const node of predicate.declaration.body.tree.nodes) {
|
|
190
|
+
if (node.kind !== 'predicate-call')
|
|
191
|
+
continue;
|
|
192
|
+
const target = scope.get(node.name);
|
|
193
|
+
if (target === undefined)
|
|
194
|
+
continue;
|
|
195
|
+
const targetConstraints = constraintsFor(target);
|
|
196
|
+
target.declaration.parameters.forEach((formal, index) => {
|
|
197
|
+
const actual = node.arguments[index];
|
|
198
|
+
if (actual === undefined)
|
|
199
|
+
return;
|
|
200
|
+
const propagated = targetConstraints.get(formal.name) ?? [];
|
|
201
|
+
const list = constraints.get(actual.name) ?? [];
|
|
202
|
+
list.push(...propagated);
|
|
203
|
+
constraints.set(actual.name, list);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
for (const parameter of predicate.declaration.parameters) {
|
|
207
|
+
const list = constraints.get(parameter.name) ?? [];
|
|
208
|
+
const first = list[0];
|
|
209
|
+
if (first !== undefined) {
|
|
210
|
+
for (const constraint of list.slice(1)) {
|
|
211
|
+
if (constraint.base !== first.base) {
|
|
212
|
+
this.#fail('SYQL5011_TYPE_CONFLICT', parameter.nameSpan, `predicate parameter ${parameter.name} is constrained as both ${typeText(first)} and ${typeText(constraint)}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (parameter.type !== undefined) {
|
|
217
|
+
for (const constraint of list) {
|
|
218
|
+
if (!isCompatible(parameter.type, constraint)) {
|
|
219
|
+
this.#fail('SYQL5011_TYPE_CONFLICT', parameter.nameSpan, `predicate parameter ${parameter.name} has type ${typeText(parameter.type)}, incompatible with nested predicate formal ${typeText(constraint)}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
cache.set(predicate.id, constraints);
|
|
225
|
+
return constraints;
|
|
226
|
+
};
|
|
227
|
+
for (const predicate of this.#predicateById.values()) {
|
|
228
|
+
constraintsFor(predicate);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
#analyzeQuery(module, query) {
|
|
232
|
+
const bindSymbols = this.#queryBindSymbols(query);
|
|
233
|
+
const ranges = new Map(query.parameters.flatMap((parameter) => parameter.kind === 'range'
|
|
234
|
+
? [[parameter.name, parameter]]
|
|
235
|
+
: []));
|
|
236
|
+
const requirements = new Map();
|
|
237
|
+
const template = this.#expandTemplate(module, query.statement.tree, new Map(), { queryBinds: bindSymbols, ranges, requirements });
|
|
238
|
+
const conditions = [];
|
|
239
|
+
this.#collectConditions(template, conditions);
|
|
240
|
+
this.#validateControls(query, conditions);
|
|
241
|
+
this.#validateBindsAndDominance(query, bindSymbols, template, conditions);
|
|
242
|
+
const resolvedTypes = this.#resolveTypeRequirements(bindSymbols, requirements);
|
|
243
|
+
return {
|
|
244
|
+
module,
|
|
245
|
+
declaration: query,
|
|
246
|
+
inputs: query.parameters.map((parameter) => {
|
|
247
|
+
const type = this.#parameterType(parameter, resolvedTypes);
|
|
248
|
+
return { parameter, ...(type === undefined ? {} : { type }) };
|
|
249
|
+
}),
|
|
250
|
+
template,
|
|
251
|
+
conditions,
|
|
252
|
+
bindTypes: resolvedTypes,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
#queryBindSymbols(query) {
|
|
256
|
+
const symbols = new Map();
|
|
257
|
+
for (const parameter of query.parameters) {
|
|
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') {
|
|
273
|
+
for (const member of parameter.members) {
|
|
274
|
+
symbols.set(member.name, {
|
|
275
|
+
name: member.name,
|
|
276
|
+
parameter,
|
|
277
|
+
controller: parameter.name,
|
|
278
|
+
...(member.type === undefined ? {} : { type: member.type }),
|
|
279
|
+
span: member.nameSpan,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
symbols.set(parameter.name, {
|
|
285
|
+
name: parameter.name,
|
|
286
|
+
parameter,
|
|
287
|
+
...(parameter.optional ? { controller: parameter.name } : {}),
|
|
288
|
+
...(parameter.type === undefined ? {} : { type: parameter.type }),
|
|
289
|
+
span: parameter.nameSpan,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return symbols;
|
|
294
|
+
}
|
|
295
|
+
#expandTemplate(module, template, substitution, context) {
|
|
296
|
+
return template.nodes.map((node) => this.#expandNode(module, node, substitution, context));
|
|
297
|
+
}
|
|
298
|
+
#expandNode(module, node, substitution, context) {
|
|
299
|
+
if (node.kind === 'raw') {
|
|
300
|
+
return {
|
|
301
|
+
kind: 'sql',
|
|
302
|
+
parts: this.#expandSqlTokens(node.tokens, substitution, context),
|
|
303
|
+
span: node.span,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (node.kind === 'when') {
|
|
307
|
+
return {
|
|
308
|
+
kind: 'when',
|
|
309
|
+
controls: node.controls,
|
|
310
|
+
explicitPresence: node.explicitPresence,
|
|
311
|
+
controlSpans: node.controlSpans,
|
|
312
|
+
body: this.#expandTemplate(module, node.body, substitution, context),
|
|
313
|
+
span: node.span,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const call = node;
|
|
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
|
+
}
|
|
325
|
+
this.#checkArity(call.arguments.length, target, call.span);
|
|
326
|
+
const targetSubstitution = new Map();
|
|
327
|
+
target.declaration.parameters.forEach((formal, index) => {
|
|
328
|
+
const argument = call.arguments[index];
|
|
329
|
+
const outer = substitution.get(argument.name);
|
|
330
|
+
const actual = outer ?? {
|
|
331
|
+
name: argument.name,
|
|
332
|
+
span: argument.span,
|
|
333
|
+
origins: [argument.span],
|
|
334
|
+
};
|
|
335
|
+
targetSubstitution.set(formal.name, actual);
|
|
336
|
+
if (formal.type !== undefined && context.requirements !== undefined) {
|
|
337
|
+
const list = context.requirements.get(actual.name) ?? [];
|
|
338
|
+
list.push(formal.type);
|
|
339
|
+
context.requirements.set(actual.name, list);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
return {
|
|
343
|
+
kind: 'predicate',
|
|
344
|
+
predicate: target,
|
|
345
|
+
body: this.#expandTemplate(target.module, target.declaration.body.tree, targetSubstitution, context),
|
|
346
|
+
span: call.span,
|
|
347
|
+
};
|
|
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
|
+
}
|
|
389
|
+
#collectConditions(nodes, out) {
|
|
390
|
+
for (const node of nodes) {
|
|
391
|
+
if (node.kind === 'when')
|
|
392
|
+
out.push(node);
|
|
393
|
+
else if (node.kind === 'predicate')
|
|
394
|
+
this.#collectConditions(node.body, out);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
#validateControls(query, conditions) {
|
|
398
|
+
const controls = new Map(query.parameters.map((item) => [item.name, item]));
|
|
399
|
+
for (const condition of conditions) {
|
|
400
|
+
condition.controls.forEach((name, index) => {
|
|
401
|
+
const parameter = controls.get(name);
|
|
402
|
+
if (parameter === undefined) {
|
|
403
|
+
this.#fail('SYQL5008_INVALID_CONTROL', condition.controlSpans[index], `unknown when control ${JSON.stringify(name)}`);
|
|
404
|
+
}
|
|
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`);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
#validateBindsAndDominance(query, symbols, template, conditions) {
|
|
418
|
+
const allUses = new Map();
|
|
419
|
+
const controlledUses = new Map();
|
|
420
|
+
const usedControls = new Set();
|
|
421
|
+
const record = (bind, active) => {
|
|
422
|
+
const symbol = symbols.get(bind.name);
|
|
423
|
+
if (symbol === undefined) {
|
|
424
|
+
this.#fail('SYQL5006_UNDECLARED_BIND', bind.span, `bind :${bind.name} is not declared by query ${query.name}`);
|
|
425
|
+
}
|
|
426
|
+
const uses = allUses.get(bind.name) ?? [];
|
|
427
|
+
uses.push(bind);
|
|
428
|
+
allUses.set(bind.name, uses);
|
|
429
|
+
if (symbol.controller !== undefined) {
|
|
430
|
+
if (!active.has(symbol.controller)) {
|
|
431
|
+
this.#fail('SYQL5009_MISSING_DOMINANCE', bind.span, `optional bind :${bind.name} must be inside when(${symbol.controller})`);
|
|
432
|
+
}
|
|
433
|
+
const names = controlledUses.get(symbol.controller) ?? new Set();
|
|
434
|
+
names.add(bind.name);
|
|
435
|
+
controlledUses.set(symbol.controller, names);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
const walk = (nodes, active) => {
|
|
439
|
+
for (const node of nodes) {
|
|
440
|
+
if (node.kind === 'sql') {
|
|
441
|
+
for (const part of node.parts) {
|
|
442
|
+
if (part.kind === 'bind')
|
|
443
|
+
record(part, active);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else if (node.kind === 'predicate') {
|
|
447
|
+
walk(node.body, active);
|
|
448
|
+
}
|
|
449
|
+
else if (node.kind === 'when') {
|
|
450
|
+
const next = new Set(active);
|
|
451
|
+
for (const control of node.controls) {
|
|
452
|
+
usedControls.add(control);
|
|
453
|
+
next.add(control);
|
|
454
|
+
}
|
|
455
|
+
walk(node.body, next);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
walk(template, new Set());
|
|
460
|
+
for (const condition of conditions) {
|
|
461
|
+
const conditionUses = new Set();
|
|
462
|
+
const collect = (nodes) => {
|
|
463
|
+
for (const node of nodes) {
|
|
464
|
+
if (node.kind === 'sql') {
|
|
465
|
+
for (const part of node.parts) {
|
|
466
|
+
if (part.kind === 'bind')
|
|
467
|
+
conditionUses.add(part.name);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
else if (node.kind === 'predicate')
|
|
471
|
+
collect(node.body);
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
collect(condition.body);
|
|
475
|
+
condition.controls.forEach((control, index) => {
|
|
476
|
+
const parameter = query.parameters.find((item) => item.name === control);
|
|
477
|
+
if (parameter?.kind === 'value' &&
|
|
478
|
+
parameter.default === undefined &&
|
|
479
|
+
!conditionUses.has(control)) {
|
|
480
|
+
this.#fail('SYQL5010_UNUSED_CONTROL', condition.controlSpans[index], `when(${control}) does not use :${control} in its body`);
|
|
481
|
+
}
|
|
482
|
+
if (parameter?.kind === 'group' &&
|
|
483
|
+
!parameter.members.some((member) => conditionUses.has(member.name))) {
|
|
484
|
+
this.#fail('SYQL5010_UNUSED_CONTROL', condition.controlSpans[index], `when(${control}) does not use a member of group ${control}`);
|
|
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
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
for (const parameter of query.parameters) {
|
|
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)) {
|
|
500
|
+
this.#unusedInput(parameter.nameSpan, parameter.name);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
else if (parameter.kind === 'group') {
|
|
504
|
+
if (!usedControls.has(parameter.name)) {
|
|
505
|
+
this.#unusedInput(parameter.nameSpan, parameter.name);
|
|
506
|
+
}
|
|
507
|
+
const uses = controlledUses.get(parameter.name) ?? new Set();
|
|
508
|
+
for (const member of parameter.members) {
|
|
509
|
+
if (!uses.has(member.name))
|
|
510
|
+
this.#unusedInput(member.nameSpan, member.name);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else if (parameter.optional) {
|
|
514
|
+
if (!usedControls.has(parameter.name) || !allUses.has(parameter.name)) {
|
|
515
|
+
this.#unusedInput(parameter.nameSpan, parameter.name);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
else if (!allUses.has(parameter.name) &&
|
|
519
|
+
!(parameter.default !== undefined && usedControls.has(parameter.name))) {
|
|
520
|
+
this.#unusedInput(parameter.nameSpan, parameter.name);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
#resolveTypeRequirements(symbols, requirements) {
|
|
525
|
+
const resolved = new Map();
|
|
526
|
+
for (const symbol of symbols.values()) {
|
|
527
|
+
if (symbol.type !== undefined)
|
|
528
|
+
resolved.set(symbol.name, symbol.type);
|
|
529
|
+
}
|
|
530
|
+
for (const [name, constraints] of requirements) {
|
|
531
|
+
const symbol = symbols.get(name);
|
|
532
|
+
if (symbol === undefined)
|
|
533
|
+
continue;
|
|
534
|
+
const first = constraints[0];
|
|
535
|
+
if (first === undefined)
|
|
536
|
+
continue;
|
|
537
|
+
for (const constraint of constraints.slice(1)) {
|
|
538
|
+
if (constraint.base !== first.base) {
|
|
539
|
+
this.#fail('SYQL5011_TYPE_CONFLICT', symbol.span, `bind :${name} is constrained as both ${typeText(first)} and ${typeText(constraint)}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const inferred = {
|
|
543
|
+
base: first.base,
|
|
544
|
+
nullable: constraints.every((constraint) => constraint.nullable),
|
|
545
|
+
span: first.span,
|
|
546
|
+
};
|
|
547
|
+
if (symbol.type !== undefined) {
|
|
548
|
+
for (const formal of constraints) {
|
|
549
|
+
if (!isCompatible(symbol.type, formal)) {
|
|
550
|
+
this.#fail('SYQL5011_TYPE_CONFLICT', symbol.span, `bind :${name} has type ${typeText(symbol.type)}, incompatible with predicate formal ${typeText(formal)}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
resolved.set(name, symbol.type);
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
resolved.set(name, inferred);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return resolved;
|
|
560
|
+
}
|
|
561
|
+
#parameterType(parameter, resolved) {
|
|
562
|
+
if (parameter.kind === 'range' || parameter.kind === 'group') {
|
|
563
|
+
// Group members carry their own types; the logical input itself has no
|
|
564
|
+
// scalar type. L3 consumes the per-member resolved map directly.
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
567
|
+
return parameter.type ?? resolved.get(parameter.name);
|
|
568
|
+
}
|
|
569
|
+
#lookupCall(module, name) {
|
|
570
|
+
return this.#scopes.get(module.file)?.get(name);
|
|
571
|
+
}
|
|
572
|
+
#checkArity(actual, target, span) {
|
|
573
|
+
const expected = target.declaration.parameters.length;
|
|
574
|
+
if (actual !== expected) {
|
|
575
|
+
this.#fail('SYQL5003_PREDICATE_ARITY', span, `${target.declaration.name} expects ${expected} argument(s), got ${actual}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
#unusedInput(span, name) {
|
|
579
|
+
this.#fail('SYQL5007_UNUSED_INPUT', span, `query input ${name} is declared but not meaningfully used`);
|
|
580
|
+
}
|
|
581
|
+
#fail(code, span, message) {
|
|
582
|
+
throw new SyqlFrontendError(code, span, message);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
/** Resolve and statically validate a complete reachable SYQL module graph. */
|
|
586
|
+
export function analyzeSyqlSemantics(graph) {
|
|
587
|
+
return new SemanticAnalyzer(graph).analyze();
|
|
588
|
+
}
|
|
589
|
+
/** Losslessly render expanded SQL/predicate nodes, retaining `when` markers. */
|
|
590
|
+
export function renderSyqlLogicalTemplate(nodes) {
|
|
591
|
+
return nodes
|
|
592
|
+
.map((node) => {
|
|
593
|
+
if (node.kind === 'sql') {
|
|
594
|
+
return node.parts
|
|
595
|
+
.map((part) => (part.kind === 'text' ? part.text : `:${part.name}`))
|
|
596
|
+
.join('');
|
|
597
|
+
}
|
|
598
|
+
if (node.kind === 'predicate') {
|
|
599
|
+
return `(${renderSyqlLogicalTemplate(node.body)})`;
|
|
600
|
+
}
|
|
601
|
+
if (node.kind === 'when') {
|
|
602
|
+
return `when(${node.controls.join(', ')}) {${renderSyqlLogicalTemplate(node.body)}}`;
|
|
603
|
+
}
|
|
604
|
+
throw new Error('unknown SYQL logical template node');
|
|
605
|
+
})
|
|
606
|
+
.join('');
|
|
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;
|