@syncular/typegen 0.5.0 → 0.6.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 +75 -33
- package/dist/cli.js +43 -17
- package/dist/emit-queries-dart.js +167 -55
- package/dist/emit-queries-kotlin.js +166 -55
- package/dist/emit-queries-swift.js +182 -57
- package/dist/emit-queries.js +289 -123
- package/dist/fmt.d.ts +2 -2
- package/dist/fmt.js +323 -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 +359 -185
- 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 +115 -63
- package/dist/query.js +60 -11
- 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 +411 -0
- package/dist/syql-semantics.d.ts +76 -0
- package/dist/syql-semantics.js +551 -0
- package/dist/syql-validator.d.ts +35 -0
- package/dist/syql-validator.js +966 -0
- package/package.json +3 -3
- package/src/cli.ts +51 -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 +413 -165
- package/src/fmt.ts +377 -303
- package/src/generate.ts +43 -9
- package/src/index.ts +3 -1
- package/src/lsp.ts +425 -215
- package/src/manifest.ts +2 -4
- package/src/query-ir.ts +52 -42
- package/src/query.ts +199 -79
- package/src/syql-lexer.ts +12 -1
- package/src/syql-lowering.ts +638 -0
- package/src/syql-semantics.ts +859 -0
- package/src/syql-validator.ts +1492 -0
- package/dist/syql.d.ts +0 -102
- package/dist/syql.js +0 -1141
- package/src/syql.ts +0 -1470
|
@@ -0,0 +1,551 @@
|
|
|
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
|
+
function predicateId(file, name) {
|
|
10
|
+
return `${file}\0${name}`;
|
|
11
|
+
}
|
|
12
|
+
function bindName(token) {
|
|
13
|
+
return token.text.slice(1);
|
|
14
|
+
}
|
|
15
|
+
function typeText(type) {
|
|
16
|
+
return `${type.base}${type.nullable ? ' | null' : ''}`;
|
|
17
|
+
}
|
|
18
|
+
function isCompatible(actual, formal) {
|
|
19
|
+
return actual.base === formal.base && (!actual.nullable || formal.nullable);
|
|
20
|
+
}
|
|
21
|
+
class SemanticAnalyzer {
|
|
22
|
+
#graph;
|
|
23
|
+
#predicateById = new Map();
|
|
24
|
+
#scopes = new Map();
|
|
25
|
+
#calls = new Map();
|
|
26
|
+
#usedPredicateParams = new Map();
|
|
27
|
+
constructor(graph) {
|
|
28
|
+
this.#graph = graph;
|
|
29
|
+
}
|
|
30
|
+
analyze() {
|
|
31
|
+
this.#buildScopes();
|
|
32
|
+
this.#resolvePredicateCalls();
|
|
33
|
+
this.#checkPredicateCycles();
|
|
34
|
+
this.#checkPredicateTypes();
|
|
35
|
+
this.#checkPredicateClosureAndUse();
|
|
36
|
+
const queries = this.#graph.modules.flatMap((module) => module.queries.map((query) => this.#analyzeQuery(module, query)));
|
|
37
|
+
return {
|
|
38
|
+
graph: this.#graph,
|
|
39
|
+
predicateScopes: this.#scopes,
|
|
40
|
+
predicates: [...this.#predicateById.values()],
|
|
41
|
+
queries,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
#buildScopes() {
|
|
45
|
+
for (const module of this.#graph.modules) {
|
|
46
|
+
const scope = new Map();
|
|
47
|
+
for (const declaration of module.predicates) {
|
|
48
|
+
const resolved = {
|
|
49
|
+
id: predicateId(module.file, declaration.name),
|
|
50
|
+
module,
|
|
51
|
+
declaration,
|
|
52
|
+
};
|
|
53
|
+
this.#predicateById.set(resolved.id, resolved);
|
|
54
|
+
scope.set(declaration.name, resolved);
|
|
55
|
+
}
|
|
56
|
+
this.#scopes.set(module.file, scope);
|
|
57
|
+
}
|
|
58
|
+
for (const edge of this.#graph.edges) {
|
|
59
|
+
const scope = this.#scopes.get(edge.from);
|
|
60
|
+
for (const item of edge.declaration.items) {
|
|
61
|
+
const target = this.#predicateById.get(predicateId(edge.to, item.imported));
|
|
62
|
+
scope.set(item.local, target);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
#resolvePredicateCalls() {
|
|
67
|
+
for (const predicate of this.#predicateById.values()) {
|
|
68
|
+
const calls = [];
|
|
69
|
+
for (const node of predicate.declaration.body.tree.nodes) {
|
|
70
|
+
if (node.kind !== 'predicate-call')
|
|
71
|
+
continue;
|
|
72
|
+
const target = this.#resolveCall(predicate.module, node.name, node.span);
|
|
73
|
+
this.#checkArity(node.arguments.length, target, node.span);
|
|
74
|
+
calls.push(target);
|
|
75
|
+
}
|
|
76
|
+
this.#calls.set(predicate.id, calls);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
#checkPredicateCycles() {
|
|
80
|
+
const states = new Map();
|
|
81
|
+
const stack = [];
|
|
82
|
+
const visit = (predicate) => {
|
|
83
|
+
const state = states.get(predicate.id);
|
|
84
|
+
if (state === 'complete')
|
|
85
|
+
return;
|
|
86
|
+
if (state === 'active') {
|
|
87
|
+
const start = stack.findIndex((item) => item.id === predicate.id);
|
|
88
|
+
const cycle = [...stack.slice(start), predicate]
|
|
89
|
+
.map((item) => `${item.declaration.name} (${item.module.file})`)
|
|
90
|
+
.join(' -> ');
|
|
91
|
+
this.#fail('SYQL5002_PREDICATE_CYCLE', predicate.declaration.nameSpan, `predicate call cycle: ${cycle}`);
|
|
92
|
+
}
|
|
93
|
+
states.set(predicate.id, 'active');
|
|
94
|
+
stack.push(predicate);
|
|
95
|
+
for (const target of this.#calls.get(predicate.id) ?? [])
|
|
96
|
+
visit(target);
|
|
97
|
+
stack.pop();
|
|
98
|
+
states.set(predicate.id, 'complete');
|
|
99
|
+
};
|
|
100
|
+
for (const predicate of this.#predicateById.values())
|
|
101
|
+
visit(predicate);
|
|
102
|
+
}
|
|
103
|
+
#checkPredicateClosureAndUse() {
|
|
104
|
+
for (const predicate of this.#predicateById.values()) {
|
|
105
|
+
const declared = new Set(predicate.declaration.parameters.map((parameter) => parameter.name));
|
|
106
|
+
for (const node of predicate.declaration.body.tree.nodes) {
|
|
107
|
+
if (node.kind === 'raw') {
|
|
108
|
+
for (const token of node.tokens) {
|
|
109
|
+
if (token.kind === 'bind' && !declared.has(bindName(token))) {
|
|
110
|
+
this.#fail('SYQL5004_CLOSED_PREDICATE', token.span, `predicate ${predicate.declaration.name} uses undeclared bind ${token.text}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (node.kind === 'predicate-call') {
|
|
115
|
+
for (const argument of node.arguments) {
|
|
116
|
+
if (!declared.has(argument.name)) {
|
|
117
|
+
this.#fail('SYQL5004_CLOSED_PREDICATE', argument.span, `predicate ${predicate.declaration.name} passes undeclared bind :${argument.name}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const computeUsed = (predicate) => {
|
|
124
|
+
const cached = this.#usedPredicateParams.get(predicate.id);
|
|
125
|
+
if (cached !== undefined)
|
|
126
|
+
return cached;
|
|
127
|
+
const used = new Set();
|
|
128
|
+
const scope = this.#scopes.get(predicate.module.file);
|
|
129
|
+
for (const node of predicate.declaration.body.tree.nodes) {
|
|
130
|
+
if (node.kind === 'raw') {
|
|
131
|
+
for (const token of node.tokens) {
|
|
132
|
+
if (token.kind === 'bind')
|
|
133
|
+
used.add(bindName(token));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else if (node.kind === 'predicate-call') {
|
|
137
|
+
const target = scope.get(node.name);
|
|
138
|
+
const targetUsed = computeUsed(target);
|
|
139
|
+
target.declaration.parameters.forEach((formal, index) => {
|
|
140
|
+
if (targetUsed.has(formal.name)) {
|
|
141
|
+
const actual = node.arguments[index];
|
|
142
|
+
if (actual !== undefined)
|
|
143
|
+
used.add(actual.name);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
this.#usedPredicateParams.set(predicate.id, used);
|
|
149
|
+
return used;
|
|
150
|
+
};
|
|
151
|
+
for (const predicate of this.#predicateById.values()) {
|
|
152
|
+
const used = computeUsed(predicate);
|
|
153
|
+
for (const parameter of predicate.declaration.parameters) {
|
|
154
|
+
if (!used.has(parameter.name)) {
|
|
155
|
+
this.#fail('SYQL5005_UNUSED_PREDICATE_PARAMETER', parameter.nameSpan, `predicate parameter ${parameter.name} is unused after expansion`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
#checkPredicateTypes() {
|
|
161
|
+
const cache = new Map();
|
|
162
|
+
const constraintsFor = (predicate) => {
|
|
163
|
+
const cached = cache.get(predicate.id);
|
|
164
|
+
if (cached !== undefined)
|
|
165
|
+
return cached;
|
|
166
|
+
const constraints = new Map();
|
|
167
|
+
for (const parameter of predicate.declaration.parameters) {
|
|
168
|
+
if (parameter.type !== undefined) {
|
|
169
|
+
constraints.set(parameter.name, [parameter.type]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const scope = this.#scopes.get(predicate.module.file);
|
|
173
|
+
for (const node of predicate.declaration.body.tree.nodes) {
|
|
174
|
+
if (node.kind !== 'predicate-call')
|
|
175
|
+
continue;
|
|
176
|
+
const target = scope.get(node.name);
|
|
177
|
+
const targetConstraints = constraintsFor(target);
|
|
178
|
+
target.declaration.parameters.forEach((formal, index) => {
|
|
179
|
+
const actual = node.arguments[index];
|
|
180
|
+
if (actual === undefined)
|
|
181
|
+
return;
|
|
182
|
+
const propagated = targetConstraints.get(formal.name) ?? [];
|
|
183
|
+
const list = constraints.get(actual.name) ?? [];
|
|
184
|
+
list.push(...propagated);
|
|
185
|
+
constraints.set(actual.name, list);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
for (const parameter of predicate.declaration.parameters) {
|
|
189
|
+
const list = constraints.get(parameter.name) ?? [];
|
|
190
|
+
const first = list[0];
|
|
191
|
+
if (first !== undefined) {
|
|
192
|
+
for (const constraint of list.slice(1)) {
|
|
193
|
+
if (constraint.base !== first.base) {
|
|
194
|
+
this.#fail('SYQL5011_TYPE_CONFLICT', parameter.nameSpan, `predicate parameter ${parameter.name} is constrained as both ${typeText(first)} and ${typeText(constraint)}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (parameter.type !== undefined) {
|
|
199
|
+
for (const constraint of list) {
|
|
200
|
+
if (!isCompatible(parameter.type, constraint)) {
|
|
201
|
+
this.#fail('SYQL5011_TYPE_CONFLICT', parameter.nameSpan, `predicate parameter ${parameter.name} has type ${typeText(parameter.type)}, incompatible with nested predicate formal ${typeText(constraint)}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
cache.set(predicate.id, constraints);
|
|
207
|
+
return constraints;
|
|
208
|
+
};
|
|
209
|
+
for (const predicate of this.#predicateById.values()) {
|
|
210
|
+
constraintsFor(predicate);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
#analyzeQuery(module, query) {
|
|
214
|
+
const bindSymbols = this.#queryBindSymbols(query);
|
|
215
|
+
const requirements = new Map();
|
|
216
|
+
const template = this.#expandTemplate(module, query.sql.body.tree, new Map(), { queryBinds: bindSymbols, requirements });
|
|
217
|
+
const conditions = [];
|
|
218
|
+
this.#collectConditions(template, conditions);
|
|
219
|
+
this.#validateControls(query, conditions);
|
|
220
|
+
this.#validateBindsAndDominance(query, bindSymbols, template, conditions);
|
|
221
|
+
const resolvedTypes = this.#resolveTypeRequirements(bindSymbols, requirements);
|
|
222
|
+
return {
|
|
223
|
+
module,
|
|
224
|
+
declaration: query,
|
|
225
|
+
inputs: query.parameters.map((parameter) => {
|
|
226
|
+
const type = this.#parameterType(parameter, resolvedTypes);
|
|
227
|
+
return { parameter, ...(type === undefined ? {} : { type }) };
|
|
228
|
+
}),
|
|
229
|
+
template,
|
|
230
|
+
conditions,
|
|
231
|
+
bindTypes: resolvedTypes,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
#queryBindSymbols(query) {
|
|
235
|
+
const symbols = new Map();
|
|
236
|
+
for (const parameter of query.parameters) {
|
|
237
|
+
if (parameter.kind === 'switch')
|
|
238
|
+
continue;
|
|
239
|
+
if (parameter.kind === 'group') {
|
|
240
|
+
for (const member of parameter.members) {
|
|
241
|
+
symbols.set(member.name, {
|
|
242
|
+
name: member.name,
|
|
243
|
+
parameter,
|
|
244
|
+
controller: parameter.name,
|
|
245
|
+
...(member.type === undefined ? {} : { type: member.type }),
|
|
246
|
+
span: member.nameSpan,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
symbols.set(parameter.name, {
|
|
252
|
+
name: parameter.name,
|
|
253
|
+
parameter,
|
|
254
|
+
...(parameter.optional ? { controller: parameter.name } : {}),
|
|
255
|
+
...(parameter.type === undefined ? {} : { type: parameter.type }),
|
|
256
|
+
span: parameter.nameSpan,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return symbols;
|
|
261
|
+
}
|
|
262
|
+
#expandTemplate(module, template, substitution, context) {
|
|
263
|
+
return template.nodes.map((node) => this.#expandNode(module, node, substitution, context));
|
|
264
|
+
}
|
|
265
|
+
#expandNode(module, node, substitution, context) {
|
|
266
|
+
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 };
|
|
294
|
+
}
|
|
295
|
+
if (node.kind === 'when') {
|
|
296
|
+
return {
|
|
297
|
+
kind: 'when',
|
|
298
|
+
controls: node.controls,
|
|
299
|
+
controlSpans: node.controlSpans,
|
|
300
|
+
body: this.#expandTemplate(module, node.body, substitution, context),
|
|
301
|
+
span: node.span,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
if (node.kind === 'scope' || node.kind === 'cover') {
|
|
305
|
+
return { kind: node.kind, directive: node, span: node.span };
|
|
306
|
+
}
|
|
307
|
+
const call = node;
|
|
308
|
+
const target = this.#resolveCall(module, call.name, call.span);
|
|
309
|
+
this.#checkArity(call.arguments.length, target, call.span);
|
|
310
|
+
const targetSubstitution = new Map();
|
|
311
|
+
target.declaration.parameters.forEach((formal, index) => {
|
|
312
|
+
const argument = call.arguments[index];
|
|
313
|
+
const outer = substitution.get(argument.name);
|
|
314
|
+
const actual = outer ?? {
|
|
315
|
+
name: argument.name,
|
|
316
|
+
span: argument.span,
|
|
317
|
+
origins: [argument.span],
|
|
318
|
+
};
|
|
319
|
+
targetSubstitution.set(formal.name, actual);
|
|
320
|
+
if (formal.type !== undefined && context.requirements !== undefined) {
|
|
321
|
+
const list = context.requirements.get(actual.name) ?? [];
|
|
322
|
+
list.push(formal.type);
|
|
323
|
+
context.requirements.set(actual.name, list);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
return {
|
|
327
|
+
kind: 'predicate',
|
|
328
|
+
predicate: target,
|
|
329
|
+
body: this.#expandTemplate(target.module, target.declaration.body.tree, targetSubstitution, context),
|
|
330
|
+
span: call.span,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
#collectConditions(nodes, out) {
|
|
334
|
+
for (const node of nodes) {
|
|
335
|
+
if (node.kind === 'when')
|
|
336
|
+
out.push(node);
|
|
337
|
+
else if (node.kind === 'predicate')
|
|
338
|
+
this.#collectConditions(node.body, out);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
#validateControls(query, conditions) {
|
|
342
|
+
const controls = new Map(query.parameters.map((item) => [item.name, item]));
|
|
343
|
+
for (const condition of conditions) {
|
|
344
|
+
condition.controls.forEach((name, index) => {
|
|
345
|
+
const parameter = controls.get(name);
|
|
346
|
+
if (parameter === undefined) {
|
|
347
|
+
this.#fail('SYQL5008_INVALID_CONTROL', condition.controlSpans[index], `unknown when control ${JSON.stringify(name)}`);
|
|
348
|
+
}
|
|
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`);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
#validateBindsAndDominance(query, symbols, template, conditions) {
|
|
356
|
+
const allUses = new Map();
|
|
357
|
+
const controlledUses = new Map();
|
|
358
|
+
const usedControls = new Set();
|
|
359
|
+
const record = (bind, active) => {
|
|
360
|
+
const symbol = symbols.get(bind.name);
|
|
361
|
+
if (symbol === undefined) {
|
|
362
|
+
this.#fail('SYQL5006_UNDECLARED_BIND', bind.span, `bind :${bind.name} is not declared by query ${query.name}`);
|
|
363
|
+
}
|
|
364
|
+
const uses = allUses.get(bind.name) ?? [];
|
|
365
|
+
uses.push(bind);
|
|
366
|
+
allUses.set(bind.name, uses);
|
|
367
|
+
if (symbol.controller !== undefined) {
|
|
368
|
+
if (!active.has(symbol.controller)) {
|
|
369
|
+
this.#fail('SYQL5009_MISSING_DOMINANCE', bind.span, `optional bind :${bind.name} must be inside when(${symbol.controller})`);
|
|
370
|
+
}
|
|
371
|
+
const names = controlledUses.get(symbol.controller) ?? new Set();
|
|
372
|
+
names.add(bind.name);
|
|
373
|
+
controlledUses.set(symbol.controller, names);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
const walk = (nodes, active) => {
|
|
377
|
+
for (const node of nodes) {
|
|
378
|
+
if (node.kind === 'sql') {
|
|
379
|
+
for (const part of node.parts) {
|
|
380
|
+
if (part.kind === 'bind')
|
|
381
|
+
record(part, active);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else if (node.kind === 'predicate') {
|
|
385
|
+
walk(node.body, active);
|
|
386
|
+
}
|
|
387
|
+
else if (node.kind === 'when') {
|
|
388
|
+
const next = new Set(active);
|
|
389
|
+
for (const control of node.controls) {
|
|
390
|
+
usedControls.add(control);
|
|
391
|
+
next.add(control);
|
|
392
|
+
}
|
|
393
|
+
walk(node.body, next);
|
|
394
|
+
}
|
|
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
|
+
}
|
|
408
|
+
};
|
|
409
|
+
walk(template, new Set());
|
|
410
|
+
for (const condition of conditions) {
|
|
411
|
+
const conditionUses = new Set();
|
|
412
|
+
const collect = (nodes) => {
|
|
413
|
+
for (const node of nodes) {
|
|
414
|
+
if (node.kind === 'sql') {
|
|
415
|
+
for (const part of node.parts) {
|
|
416
|
+
if (part.kind === 'bind')
|
|
417
|
+
conditionUses.add(part.name);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else if (node.kind === 'predicate')
|
|
421
|
+
collect(node.body);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
collect(condition.body);
|
|
425
|
+
condition.controls.forEach((control, index) => {
|
|
426
|
+
const parameter = query.parameters.find((item) => item.name === control);
|
|
427
|
+
if (parameter?.kind === 'value' && !conditionUses.has(control)) {
|
|
428
|
+
this.#fail('SYQL5010_UNUSED_CONTROL', condition.controlSpans[index], `when(${control}) does not use :${control} in its body`);
|
|
429
|
+
}
|
|
430
|
+
if (parameter?.kind === 'group' &&
|
|
431
|
+
!parameter.members.some((member) => conditionUses.has(member.name))) {
|
|
432
|
+
this.#fail('SYQL5010_UNUSED_CONTROL', condition.controlSpans[index], `when(${control}) does not use a member of group ${control}`);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
for (const parameter of query.parameters) {
|
|
437
|
+
if (parameter.kind === 'switch') {
|
|
438
|
+
if (!usedControls.has(parameter.name)) {
|
|
439
|
+
this.#unusedInput(parameter.nameSpan, parameter.name);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
else if (parameter.kind === 'group') {
|
|
443
|
+
if (!usedControls.has(parameter.name)) {
|
|
444
|
+
this.#unusedInput(parameter.nameSpan, parameter.name);
|
|
445
|
+
}
|
|
446
|
+
const uses = controlledUses.get(parameter.name) ?? new Set();
|
|
447
|
+
for (const member of parameter.members) {
|
|
448
|
+
if (!uses.has(member.name))
|
|
449
|
+
this.#unusedInput(member.nameSpan, member.name);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
else if (parameter.optional) {
|
|
453
|
+
if (!usedControls.has(parameter.name) || !allUses.has(parameter.name)) {
|
|
454
|
+
this.#unusedInput(parameter.nameSpan, parameter.name);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else if (!allUses.has(parameter.name)) {
|
|
458
|
+
this.#unusedInput(parameter.nameSpan, parameter.name);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
#resolveTypeRequirements(symbols, requirements) {
|
|
463
|
+
const resolved = new Map();
|
|
464
|
+
for (const symbol of symbols.values()) {
|
|
465
|
+
if (symbol.type !== undefined)
|
|
466
|
+
resolved.set(symbol.name, symbol.type);
|
|
467
|
+
}
|
|
468
|
+
for (const [name, constraints] of requirements) {
|
|
469
|
+
const symbol = symbols.get(name);
|
|
470
|
+
if (symbol === undefined)
|
|
471
|
+
continue;
|
|
472
|
+
const first = constraints[0];
|
|
473
|
+
if (first === undefined)
|
|
474
|
+
continue;
|
|
475
|
+
for (const constraint of constraints.slice(1)) {
|
|
476
|
+
if (constraint.base !== first.base) {
|
|
477
|
+
this.#fail('SYQL5011_TYPE_CONFLICT', symbol.span, `bind :${name} is constrained as both ${typeText(first)} and ${typeText(constraint)}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const inferred = {
|
|
481
|
+
base: first.base,
|
|
482
|
+
nullable: constraints.every((constraint) => constraint.nullable),
|
|
483
|
+
span: first.span,
|
|
484
|
+
};
|
|
485
|
+
if (symbol.type !== undefined) {
|
|
486
|
+
for (const formal of constraints) {
|
|
487
|
+
if (!isCompatible(symbol.type, formal)) {
|
|
488
|
+
this.#fail('SYQL5011_TYPE_CONFLICT', symbol.span, `bind :${name} has type ${typeText(symbol.type)}, incompatible with predicate formal ${typeText(formal)}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
resolved.set(name, symbol.type);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
resolved.set(name, inferred);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return resolved;
|
|
498
|
+
}
|
|
499
|
+
#parameterType(parameter, resolved) {
|
|
500
|
+
if (parameter.kind === 'switch')
|
|
501
|
+
return undefined;
|
|
502
|
+
if (parameter.kind === 'group') {
|
|
503
|
+
// Group members carry their own types; the logical input itself has no
|
|
504
|
+
// scalar type. L3 consumes the per-member resolved map directly.
|
|
505
|
+
return undefined;
|
|
506
|
+
}
|
|
507
|
+
return parameter.type ?? resolved.get(parameter.name);
|
|
508
|
+
}
|
|
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;
|
|
515
|
+
}
|
|
516
|
+
#checkArity(actual, target, span) {
|
|
517
|
+
const expected = target.declaration.parameters.length;
|
|
518
|
+
if (actual !== expected) {
|
|
519
|
+
this.#fail('SYQL5003_PREDICATE_ARITY', span, `@${target.declaration.name} expects ${expected} argument(s), got ${actual}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
#unusedInput(span, name) {
|
|
523
|
+
this.#fail('SYQL5007_UNUSED_INPUT', span, `query input ${name} is declared but not meaningfully used`);
|
|
524
|
+
}
|
|
525
|
+
#fail(code, span, message) {
|
|
526
|
+
throw new SyqlFrontendError(code, span, message);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
/** Resolve and statically validate a complete reachable SYQL module graph. */
|
|
530
|
+
export function analyzeSyqlSemantics(graph) {
|
|
531
|
+
return new SemanticAnalyzer(graph).analyze();
|
|
532
|
+
}
|
|
533
|
+
/** Losslessly render expanded SQL/predicate nodes, retaining `when` markers. */
|
|
534
|
+
export function renderSyqlLogicalTemplate(nodes) {
|
|
535
|
+
return nodes
|
|
536
|
+
.map((node) => {
|
|
537
|
+
if (node.kind === 'sql') {
|
|
538
|
+
return node.parts
|
|
539
|
+
.map((part) => (part.kind === 'text' ? part.text : `:${part.name}`))
|
|
540
|
+
.join('');
|
|
541
|
+
}
|
|
542
|
+
if (node.kind === 'predicate') {
|
|
543
|
+
return `(${renderSyqlLogicalTemplate(node.body)})`;
|
|
544
|
+
}
|
|
545
|
+
if (node.kind === 'when') {
|
|
546
|
+
return `when(${node.controls.join(', ')}) {${renderSyqlLogicalTemplate(node.body)}}`;
|
|
547
|
+
}
|
|
548
|
+
return node.directive.tokens.map((token) => token.text).join('');
|
|
549
|
+
})
|
|
550
|
+
.join('');
|
|
551
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Schema-aware revision-1 SYQL validation (§§5, 8–13). */
|
|
2
|
+
import type { IrDocument } from './ir.js';
|
|
3
|
+
import { type AnalyzedQuery, type QueryDb, type QueryNamingOptions, type QueryReactiveMetadata } from './query.js';
|
|
4
|
+
import type { SyqlValueType } from './syql-parser.js';
|
|
5
|
+
import type { SyqlLogicalQuery, SyqlSemanticProgram } from './syql-semantics.js';
|
|
6
|
+
export type SyqlValidationErrorCode = 'SYQL6001_INVALID_PLACEMENT' | 'SYQL6002_INVALID_SQL' | 'SYQL6003_NONDETERMINISTIC_SQL' | 'SYQL6004_TYPE_CONFLICT' | 'SYQL6005_INVALID_REACTIVE_DIRECTIVE' | 'SYQL6006_INVALID_SORT' | 'SYQL6007_INVALID_PAGE' | 'SYQL6008_INVALID_IDENTITY';
|
|
7
|
+
export interface SyqlValidatedSort {
|
|
8
|
+
readonly control: string;
|
|
9
|
+
readonly defaultProfile: string;
|
|
10
|
+
readonly profiles: readonly {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly sql: string;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
export interface SyqlValidatedQuery {
|
|
16
|
+
readonly logical: SyqlLogicalQuery;
|
|
17
|
+
/** Reference realization: every conditional active, default controls. */
|
|
18
|
+
readonly referenceSql: string;
|
|
19
|
+
readonly analysis: AnalyzedQuery;
|
|
20
|
+
readonly bindTypes: ReadonlyMap<string, SyqlValueType>;
|
|
21
|
+
readonly reactive: QueryReactiveMetadata;
|
|
22
|
+
readonly identity?: readonly string[];
|
|
23
|
+
readonly sort?: SyqlValidatedSort;
|
|
24
|
+
readonly page?: {
|
|
25
|
+
readonly control: string;
|
|
26
|
+
readonly defaultSize: number;
|
|
27
|
+
readonly maxSize: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface SyqlValidatedProgram {
|
|
31
|
+
readonly semantic: SyqlSemanticProgram;
|
|
32
|
+
readonly queries: readonly SyqlValidatedQuery[];
|
|
33
|
+
}
|
|
34
|
+
/** Validate a semantic SYQL program against schema IR and SQLite. */
|
|
35
|
+
export declare function validateSyqlProgram(semantic: SyqlSemanticProgram, ir: IrDocument, db: QueryDb, naming?: QueryNamingOptions): SyqlValidatedProgram;
|