@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.
Files changed (56) hide show
  1. package/README.md +79 -32
  2. package/dist/cli.js +40 -17
  3. package/dist/emit-queries-dart.js +168 -55
  4. package/dist/emit-queries-kotlin.js +167 -55
  5. package/dist/emit-queries-swift.js +183 -57
  6. package/dist/emit-queries.js +286 -123
  7. package/dist/fmt.d.ts +2 -2
  8. package/dist/fmt.js +348 -316
  9. package/dist/generate.d.ts +1 -1
  10. package/dist/generate.js +28 -9
  11. package/dist/index.d.ts +3 -1
  12. package/dist/index.js +3 -1
  13. package/dist/lsp.d.ts +1 -3
  14. package/dist/lsp.js +349 -187
  15. package/dist/manifest.d.ts +1 -3
  16. package/dist/manifest.js +1 -1
  17. package/dist/query-ir.js +53 -42
  18. package/dist/query.d.ts +110 -63
  19. package/dist/query.js +89 -11
  20. package/dist/syql-ast.d.ts +12 -13
  21. package/dist/syql-ast.js +26 -20
  22. package/dist/syql-lexer.d.ts +2 -0
  23. package/dist/syql-lexer.js +9 -2
  24. package/dist/syql-lowering.d.ts +22 -0
  25. package/dist/syql-lowering.js +434 -0
  26. package/dist/syql-parser.d.ts +19 -18
  27. package/dist/syql-parser.js +341 -93
  28. package/dist/syql-semantics.d.ts +73 -0
  29. package/dist/syql-semantics.js +607 -0
  30. package/dist/syql-template-parser.d.ts +5 -20
  31. package/dist/syql-template-parser.js +116 -109
  32. package/dist/syql-validator.d.ts +35 -0
  33. package/dist/syql-validator.js +993 -0
  34. package/package.json +3 -3
  35. package/src/cli.ts +49 -21
  36. package/src/emit-queries-dart.ts +195 -69
  37. package/src/emit-queries-kotlin.ts +197 -69
  38. package/src/emit-queries-swift.ts +224 -68
  39. package/src/emit-queries.ts +410 -165
  40. package/src/fmt.ts +405 -303
  41. package/src/generate.ts +43 -9
  42. package/src/index.ts +3 -1
  43. package/src/lsp.ts +414 -216
  44. package/src/manifest.ts +2 -4
  45. package/src/query-ir.ts +52 -42
  46. package/src/query.ts +229 -79
  47. package/src/syql-ast.ts +38 -34
  48. package/src/syql-lexer.ts +12 -1
  49. package/src/syql-lowering.ts +664 -0
  50. package/src/syql-parser.ts +472 -134
  51. package/src/syql-semantics.ts +930 -0
  52. package/src/syql-template-parser.ts +119 -163
  53. package/src/syql-validator.ts +1486 -0
  54. package/dist/syql.d.ts +0 -102
  55. package/dist/syql.js +0 -1141
  56. package/src/syql.ts +0 -1470
@@ -0,0 +1,930 @@
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 {
9
+ SyqlFrontendError,
10
+ type SyqlSourceSpan,
11
+ type SyqlToken,
12
+ } from './syql-lexer';
13
+ import type { SyqlModuleGraph } from './syql-modules';
14
+ import type {
15
+ SyqlPredicateDeclaration,
16
+ SyqlQueryDeclaration,
17
+ SyqlQueryParameter,
18
+ SyqlRangeParameter,
19
+ SyqlSyntaxFile,
20
+ SyqlValueType,
21
+ } from './syql-parser';
22
+ import type {
23
+ SyqlEmbeddedNode,
24
+ SyqlEmbeddedTemplate,
25
+ SyqlPredicateCall,
26
+ } from './syql-template-parser';
27
+
28
+ export type SyqlSemanticErrorCode =
29
+ | 'SYQL5001_UNKNOWN_PREDICATE'
30
+ | 'SYQL5002_PREDICATE_CYCLE'
31
+ | 'SYQL5003_PREDICATE_ARITY'
32
+ | 'SYQL5004_CLOSED_PREDICATE'
33
+ | 'SYQL5005_UNUSED_PREDICATE_PARAMETER'
34
+ | 'SYQL5006_UNDECLARED_BIND'
35
+ | 'SYQL5007_UNUSED_INPUT'
36
+ | 'SYQL5008_INVALID_CONTROL'
37
+ | 'SYQL5009_MISSING_DOMINANCE'
38
+ | 'SYQL5010_UNUSED_CONTROL'
39
+ | 'SYQL5011_TYPE_CONFLICT';
40
+
41
+ export interface SyqlResolvedPredicate {
42
+ readonly id: string;
43
+ readonly module: SyqlSyntaxFile;
44
+ readonly declaration: SyqlPredicateDeclaration;
45
+ }
46
+
47
+ export interface SyqlLogicalBind {
48
+ readonly kind: 'bind';
49
+ readonly name: string;
50
+ readonly span: SyqlSourceSpan;
51
+ /** Outermost call-site first, authored bind last. */
52
+ readonly origins: readonly SyqlSourceSpan[];
53
+ }
54
+
55
+ export type SyqlSqlPart =
56
+ | { readonly kind: 'text'; readonly text: string }
57
+ | SyqlLogicalBind;
58
+
59
+ export interface SyqlLogicalSqlNode {
60
+ readonly kind: 'sql';
61
+ readonly parts: readonly SyqlSqlPart[];
62
+ readonly span: SyqlSourceSpan;
63
+ }
64
+
65
+ export interface SyqlLogicalPredicateNode {
66
+ readonly kind: 'predicate';
67
+ readonly predicate: SyqlResolvedPredicate;
68
+ readonly body: readonly SyqlLogicalTemplateNode[];
69
+ readonly span: SyqlSourceSpan;
70
+ }
71
+
72
+ export interface SyqlLogicalWhenNode {
73
+ readonly kind: 'when';
74
+ readonly controls: readonly string[];
75
+ readonly explicitPresence: readonly boolean[];
76
+ readonly controlSpans: readonly SyqlSourceSpan[];
77
+ readonly body: readonly SyqlLogicalTemplateNode[];
78
+ readonly span: SyqlSourceSpan;
79
+ }
80
+
81
+ export type SyqlLogicalTemplateNode =
82
+ | SyqlLogicalSqlNode
83
+ | SyqlLogicalPredicateNode
84
+ | SyqlLogicalWhenNode;
85
+
86
+ export interface SyqlLogicalInput {
87
+ readonly parameter: SyqlQueryParameter;
88
+ /** Type declared in source or constrained by annotated predicate formals. */
89
+ readonly type?: SyqlValueType;
90
+ }
91
+
92
+ export interface SyqlLogicalQuery {
93
+ readonly module: SyqlSyntaxFile;
94
+ readonly declaration: SyqlQueryDeclaration;
95
+ readonly inputs: readonly SyqlLogicalInput[];
96
+ readonly template: readonly SyqlLogicalTemplateNode[];
97
+ readonly conditions: readonly SyqlLogicalWhenNode[];
98
+ /** Resolved source/predicate types by scalar or group-member bind name. */
99
+ readonly bindTypes: ReadonlyMap<string, SyqlValueType>;
100
+ }
101
+
102
+ export interface SyqlSemanticProgram {
103
+ readonly graph: SyqlModuleGraph;
104
+ readonly predicateScopes: ReadonlyMap<
105
+ string,
106
+ ReadonlyMap<string, SyqlResolvedPredicate>
107
+ >;
108
+ readonly predicates: readonly SyqlResolvedPredicate[];
109
+ readonly queries: readonly SyqlLogicalQuery[];
110
+ }
111
+
112
+ interface ActualBind {
113
+ readonly name: string;
114
+ readonly span: SyqlSourceSpan;
115
+ readonly origins: readonly SyqlSourceSpan[];
116
+ }
117
+
118
+ interface QueryBindSymbol {
119
+ readonly name: string;
120
+ readonly parameter: SyqlQueryParameter;
121
+ readonly controller?: string;
122
+ readonly type?: SyqlValueType;
123
+ readonly span: SyqlSourceSpan;
124
+ }
125
+
126
+ interface ExpansionContext {
127
+ readonly queryBinds?: ReadonlyMap<string, QueryBindSymbol>;
128
+ readonly ranges?: ReadonlyMap<string, SyqlRangeParameter>;
129
+ readonly requirements?: Map<string, SyqlValueType[]>;
130
+ }
131
+
132
+ export function syqlRangeStartBind(name: string): string {
133
+ return `__syqlRangeStart_${name}`;
134
+ }
135
+
136
+ export function syqlRangeEndBind(name: string): string {
137
+ return `__syqlRangeEnd_${name}`;
138
+ }
139
+
140
+ function predicateId(file: string, name: string): string {
141
+ return `${file}\0${name}`;
142
+ }
143
+
144
+ function bindName(token: SyqlToken): string {
145
+ return token.text.slice(1);
146
+ }
147
+
148
+ function typeText(type: SyqlValueType): string {
149
+ return `${type.base}${type.nullable ? ' | null' : ''}`;
150
+ }
151
+
152
+ function isCompatible(actual: SyqlValueType, formal: SyqlValueType): boolean {
153
+ return actual.base === formal.base && (!actual.nullable || formal.nullable);
154
+ }
155
+
156
+ class SemanticAnalyzer {
157
+ readonly #graph: SyqlModuleGraph;
158
+ readonly #predicateById = new Map<string, SyqlResolvedPredicate>();
159
+ readonly #scopes = new Map<string, Map<string, SyqlResolvedPredicate>>();
160
+ readonly #calls = new Map<string, readonly SyqlResolvedPredicate[]>();
161
+ readonly #usedPredicateParams = new Map<string, ReadonlySet<string>>();
162
+
163
+ constructor(graph: SyqlModuleGraph) {
164
+ this.#graph = graph;
165
+ }
166
+
167
+ analyze(): SyqlSemanticProgram {
168
+ this.#buildScopes();
169
+ this.#resolvePredicateCalls();
170
+ this.#checkPredicateCycles();
171
+ this.#checkPredicateTypes();
172
+ this.#checkPredicateClosureAndUse();
173
+
174
+ const queries = this.#graph.modules.flatMap((module) =>
175
+ module.queries.map((query) => this.#analyzeQuery(module, query)),
176
+ );
177
+ return {
178
+ graph: this.#graph,
179
+ predicateScopes: this.#scopes,
180
+ predicates: [...this.#predicateById.values()],
181
+ queries,
182
+ };
183
+ }
184
+
185
+ #buildScopes(): void {
186
+ for (const module of this.#graph.modules) {
187
+ const scope = new Map<string, SyqlResolvedPredicate>();
188
+ for (const declaration of module.predicates) {
189
+ const resolved = {
190
+ id: predicateId(module.file, declaration.name),
191
+ module,
192
+ declaration,
193
+ };
194
+ this.#predicateById.set(resolved.id, resolved);
195
+ scope.set(declaration.name, resolved);
196
+ }
197
+ this.#scopes.set(module.file, scope);
198
+ }
199
+
200
+ for (const edge of this.#graph.edges) {
201
+ const scope = this.#scopes.get(edge.from) as Map<
202
+ string,
203
+ SyqlResolvedPredicate
204
+ >;
205
+ for (const item of edge.declaration.items) {
206
+ const target = this.#predicateById.get(
207
+ predicateId(edge.to, item.imported),
208
+ ) as SyqlResolvedPredicate;
209
+ scope.set(item.local, target);
210
+ }
211
+ }
212
+ }
213
+
214
+ #resolvePredicateCalls(): void {
215
+ for (const predicate of this.#predicateById.values()) {
216
+ const calls: SyqlResolvedPredicate[] = [];
217
+ for (const node of predicate.declaration.body.tree.nodes) {
218
+ if (node.kind !== 'predicate-call') continue;
219
+ const target = this.#lookupCall(predicate.module, node.name);
220
+ if (target === undefined) continue;
221
+ this.#checkArity(node.arguments.length, target, node.span);
222
+ calls.push(target);
223
+ }
224
+ this.#calls.set(predicate.id, calls);
225
+ }
226
+ }
227
+
228
+ #checkPredicateCycles(): void {
229
+ const states = new Map<string, 'active' | 'complete'>();
230
+ const stack: SyqlResolvedPredicate[] = [];
231
+
232
+ const visit = (predicate: SyqlResolvedPredicate): void => {
233
+ const state = states.get(predicate.id);
234
+ if (state === 'complete') return;
235
+ if (state === 'active') {
236
+ const start = stack.findIndex((item) => item.id === predicate.id);
237
+ const cycle = [...stack.slice(start), predicate]
238
+ .map((item) => `${item.declaration.name} (${item.module.file})`)
239
+ .join(' -> ');
240
+ this.#fail(
241
+ 'SYQL5002_PREDICATE_CYCLE',
242
+ predicate.declaration.nameSpan,
243
+ `predicate call cycle: ${cycle}`,
244
+ );
245
+ }
246
+ states.set(predicate.id, 'active');
247
+ stack.push(predicate);
248
+ for (const target of this.#calls.get(predicate.id) ?? []) visit(target);
249
+ stack.pop();
250
+ states.set(predicate.id, 'complete');
251
+ };
252
+
253
+ for (const predicate of this.#predicateById.values()) visit(predicate);
254
+ }
255
+
256
+ #checkPredicateClosureAndUse(): void {
257
+ for (const predicate of this.#predicateById.values()) {
258
+ const declared = new Set(
259
+ predicate.declaration.parameters.map((parameter) => parameter.name),
260
+ );
261
+ for (const node of predicate.declaration.body.tree.nodes) {
262
+ if (node.kind === 'raw') {
263
+ for (const token of node.tokens) {
264
+ if (token.kind === 'bind' && !declared.has(bindName(token))) {
265
+ this.#fail(
266
+ 'SYQL5004_CLOSED_PREDICATE',
267
+ token.span,
268
+ `predicate ${predicate.declaration.name} uses undeclared bind ${token.text}`,
269
+ );
270
+ }
271
+ }
272
+ } else if (node.kind === 'predicate-call') {
273
+ const target = this.#lookupCall(predicate.module, node.name);
274
+ for (const argument of node.arguments) {
275
+ if (!declared.has(argument.name)) {
276
+ this.#fail(
277
+ 'SYQL5004_CLOSED_PREDICATE',
278
+ argument.span,
279
+ `predicate ${predicate.declaration.name} passes undeclared bind :${argument.name}`,
280
+ );
281
+ }
282
+ }
283
+ if (target === undefined) continue;
284
+ }
285
+ }
286
+ }
287
+
288
+ const computeUsed = (
289
+ predicate: SyqlResolvedPredicate,
290
+ ): ReadonlySet<string> => {
291
+ const cached = this.#usedPredicateParams.get(predicate.id);
292
+ if (cached !== undefined) return cached;
293
+ const used = new Set<string>();
294
+ const scope = this.#scopes.get(predicate.module.file) as ReadonlyMap<
295
+ string,
296
+ SyqlResolvedPredicate
297
+ >;
298
+ for (const node of predicate.declaration.body.tree.nodes) {
299
+ if (node.kind === 'raw') {
300
+ for (const token of node.tokens) {
301
+ if (token.kind === 'bind') used.add(bindName(token));
302
+ }
303
+ } else if (node.kind === 'predicate-call') {
304
+ const target = scope.get(node.name);
305
+ if (target === undefined) {
306
+ for (const argument of node.arguments) used.add(argument.name);
307
+ continue;
308
+ }
309
+ const targetUsed = computeUsed(target);
310
+ target.declaration.parameters.forEach((formal, index) => {
311
+ if (targetUsed.has(formal.name)) {
312
+ const actual = node.arguments[index];
313
+ if (actual !== undefined) used.add(actual.name);
314
+ }
315
+ });
316
+ }
317
+ }
318
+ this.#usedPredicateParams.set(predicate.id, used);
319
+ return used;
320
+ };
321
+
322
+ for (const predicate of this.#predicateById.values()) {
323
+ const used = computeUsed(predicate);
324
+ for (const parameter of predicate.declaration.parameters) {
325
+ if (!used.has(parameter.name)) {
326
+ this.#fail(
327
+ 'SYQL5005_UNUSED_PREDICATE_PARAMETER',
328
+ parameter.nameSpan,
329
+ `predicate parameter ${parameter.name} is unused after expansion`,
330
+ );
331
+ }
332
+ }
333
+ }
334
+ }
335
+
336
+ #checkPredicateTypes(): void {
337
+ const cache = new Map<
338
+ string,
339
+ ReadonlyMap<string, readonly SyqlValueType[]>
340
+ >();
341
+
342
+ const constraintsFor = (
343
+ predicate: SyqlResolvedPredicate,
344
+ ): ReadonlyMap<string, readonly SyqlValueType[]> => {
345
+ const cached = cache.get(predicate.id);
346
+ if (cached !== undefined) return cached;
347
+ const constraints = new Map<string, SyqlValueType[]>();
348
+ for (const parameter of predicate.declaration.parameters) {
349
+ if (parameter.type !== undefined) {
350
+ constraints.set(parameter.name, [parameter.type]);
351
+ }
352
+ }
353
+ const scope = this.#scopes.get(predicate.module.file) as ReadonlyMap<
354
+ string,
355
+ SyqlResolvedPredicate
356
+ >;
357
+ for (const node of predicate.declaration.body.tree.nodes) {
358
+ if (node.kind !== 'predicate-call') continue;
359
+ const target = scope.get(node.name);
360
+ if (target === undefined) continue;
361
+ const targetConstraints = constraintsFor(target);
362
+ target.declaration.parameters.forEach((formal, index) => {
363
+ const actual = node.arguments[index];
364
+ if (actual === undefined) return;
365
+ const propagated = targetConstraints.get(formal.name) ?? [];
366
+ const list = constraints.get(actual.name) ?? [];
367
+ list.push(...propagated);
368
+ constraints.set(actual.name, list);
369
+ });
370
+ }
371
+
372
+ for (const parameter of predicate.declaration.parameters) {
373
+ const list = constraints.get(parameter.name) ?? [];
374
+ const first = list[0];
375
+ if (first !== undefined) {
376
+ for (const constraint of list.slice(1)) {
377
+ if (constraint.base !== first.base) {
378
+ this.#fail(
379
+ 'SYQL5011_TYPE_CONFLICT',
380
+ parameter.nameSpan,
381
+ `predicate parameter ${parameter.name} is constrained as both ${typeText(first)} and ${typeText(constraint)}`,
382
+ );
383
+ }
384
+ }
385
+ }
386
+ if (parameter.type !== undefined) {
387
+ for (const constraint of list) {
388
+ if (!isCompatible(parameter.type, constraint)) {
389
+ this.#fail(
390
+ 'SYQL5011_TYPE_CONFLICT',
391
+ parameter.nameSpan,
392
+ `predicate parameter ${parameter.name} has type ${typeText(parameter.type)}, incompatible with nested predicate formal ${typeText(constraint)}`,
393
+ );
394
+ }
395
+ }
396
+ }
397
+ }
398
+ cache.set(predicate.id, constraints);
399
+ return constraints;
400
+ };
401
+
402
+ for (const predicate of this.#predicateById.values()) {
403
+ constraintsFor(predicate);
404
+ }
405
+ }
406
+
407
+ #analyzeQuery(
408
+ module: SyqlSyntaxFile,
409
+ query: SyqlQueryDeclaration,
410
+ ): SyqlLogicalQuery {
411
+ const bindSymbols = this.#queryBindSymbols(query);
412
+ const ranges = new Map(
413
+ query.parameters.flatMap((parameter) =>
414
+ parameter.kind === 'range'
415
+ ? [[parameter.name, parameter] as const]
416
+ : [],
417
+ ),
418
+ );
419
+ const requirements = new Map<string, SyqlValueType[]>();
420
+ const template = this.#expandTemplate(
421
+ module,
422
+ query.statement.tree,
423
+ new Map(),
424
+ { queryBinds: bindSymbols, ranges, requirements },
425
+ );
426
+ const conditions: SyqlLogicalWhenNode[] = [];
427
+ this.#collectConditions(template, conditions);
428
+ this.#validateControls(query, conditions);
429
+ this.#validateBindsAndDominance(query, bindSymbols, template, conditions);
430
+ const resolvedTypes = this.#resolveTypeRequirements(
431
+ bindSymbols,
432
+ requirements,
433
+ );
434
+ return {
435
+ module,
436
+ declaration: query,
437
+ inputs: query.parameters.map((parameter) => {
438
+ const type = this.#parameterType(parameter, resolvedTypes);
439
+ return { parameter, ...(type === undefined ? {} : { type }) };
440
+ }),
441
+ template,
442
+ conditions,
443
+ bindTypes: resolvedTypes,
444
+ };
445
+ }
446
+
447
+ #queryBindSymbols(
448
+ query: SyqlQueryDeclaration,
449
+ ): ReadonlyMap<string, QueryBindSymbol> {
450
+ const symbols = new Map<string, QueryBindSymbol>();
451
+ for (const parameter of query.parameters) {
452
+ if (parameter.kind === 'range') {
453
+ for (const name of [
454
+ syqlRangeStartBind(parameter.name),
455
+ syqlRangeEndBind(parameter.name),
456
+ ]) {
457
+ symbols.set(name, {
458
+ name,
459
+ parameter,
460
+ ...(parameter.optional ? { controller: parameter.name } : {}),
461
+ ...(parameter.type === undefined ? {} : { type: parameter.type }),
462
+ span: parameter.nameSpan,
463
+ });
464
+ }
465
+ } else if (parameter.kind === 'group') {
466
+ for (const member of parameter.members) {
467
+ symbols.set(member.name, {
468
+ name: member.name,
469
+ parameter,
470
+ controller: parameter.name,
471
+ ...(member.type === undefined ? {} : { type: member.type }),
472
+ span: member.nameSpan,
473
+ });
474
+ }
475
+ } else {
476
+ symbols.set(parameter.name, {
477
+ name: parameter.name,
478
+ parameter,
479
+ ...(parameter.optional ? { controller: parameter.name } : {}),
480
+ ...(parameter.type === undefined ? {} : { type: parameter.type }),
481
+ span: parameter.nameSpan,
482
+ });
483
+ }
484
+ }
485
+ return symbols;
486
+ }
487
+
488
+ #expandTemplate(
489
+ module: SyqlSyntaxFile,
490
+ template: SyqlEmbeddedTemplate,
491
+ substitution: ReadonlyMap<string, ActualBind>,
492
+ context: ExpansionContext,
493
+ ): readonly SyqlLogicalTemplateNode[] {
494
+ return template.nodes.map((node) =>
495
+ this.#expandNode(module, node, substitution, context),
496
+ );
497
+ }
498
+
499
+ #expandNode(
500
+ module: SyqlSyntaxFile,
501
+ node: SyqlEmbeddedNode,
502
+ substitution: ReadonlyMap<string, ActualBind>,
503
+ context: ExpansionContext,
504
+ ): SyqlLogicalTemplateNode {
505
+ if (node.kind === 'raw') {
506
+ return {
507
+ kind: 'sql',
508
+ parts: this.#expandSqlTokens(node.tokens, substitution, context),
509
+ span: node.span,
510
+ };
511
+ }
512
+ if (node.kind === 'when') {
513
+ return {
514
+ kind: 'when',
515
+ controls: node.controls,
516
+ explicitPresence: node.explicitPresence,
517
+ controlSpans: node.controlSpans,
518
+ body: this.#expandTemplate(module, node.body, substitution, context),
519
+ span: node.span,
520
+ };
521
+ }
522
+ const call = node as SyqlPredicateCall;
523
+ const target = this.#lookupCall(module, call.name);
524
+ if (target === undefined) {
525
+ return {
526
+ kind: 'sql',
527
+ parts: this.#expandSqlTokens(call.tokens, substitution, context),
528
+ span: call.span,
529
+ };
530
+ }
531
+ this.#checkArity(call.arguments.length, target, call.span);
532
+ const targetSubstitution = new Map<string, ActualBind>();
533
+ target.declaration.parameters.forEach((formal, index) => {
534
+ const argument = call.arguments[index] as (typeof call.arguments)[number];
535
+ const outer = substitution.get(argument.name);
536
+ const actual: ActualBind = outer ?? {
537
+ name: argument.name,
538
+ span: argument.span,
539
+ origins: [argument.span],
540
+ };
541
+ targetSubstitution.set(formal.name, actual);
542
+ if (formal.type !== undefined && context.requirements !== undefined) {
543
+ const list = context.requirements.get(actual.name) ?? [];
544
+ list.push(formal.type);
545
+ context.requirements.set(actual.name, list);
546
+ }
547
+ });
548
+ return {
549
+ kind: 'predicate',
550
+ predicate: target,
551
+ body: this.#expandTemplate(
552
+ target.module,
553
+ target.declaration.body.tree,
554
+ targetSubstitution,
555
+ context,
556
+ ),
557
+ span: call.span,
558
+ };
559
+ }
560
+
561
+ #expandSqlTokens(
562
+ tokens: readonly SyqlToken[],
563
+ substitution: ReadonlyMap<string, ActualBind>,
564
+ context: ExpansionContext,
565
+ ): readonly SyqlSqlPart[] {
566
+ const parts: SyqlSqlPart[] = [];
567
+ const pushText = (text: string): void => {
568
+ const previous = parts[parts.length - 1];
569
+ if (previous?.kind === 'text') {
570
+ parts[parts.length - 1] = { kind: 'text', text: previous.text + text };
571
+ } else parts.push({ kind: 'text', text });
572
+ };
573
+ const pushBind = (
574
+ name: string,
575
+ token: SyqlToken,
576
+ origins: readonly SyqlSourceSpan[],
577
+ ): void => {
578
+ parts.push({ kind: 'bind', name, span: token.span, origins });
579
+ };
580
+ for (const token of tokens) {
581
+ if (token.kind !== 'bind') {
582
+ pushText(token.text);
583
+ continue;
584
+ }
585
+ const authored = bindName(token);
586
+ const substituted = substitution.get(authored);
587
+ if (substituted !== undefined) {
588
+ const parameter = context.ranges?.get(substituted.name);
589
+ if (parameter !== undefined) {
590
+ this.#fail(
591
+ 'SYQL5011_TYPE_CONFLICT',
592
+ token.span,
593
+ `range input ${parameter.name} cannot be passed to a predicate`,
594
+ );
595
+ }
596
+ pushBind(substituted.name, token, [...substituted.origins, token.span]);
597
+ continue;
598
+ }
599
+ const range = context.ranges?.get(authored);
600
+ if (range !== undefined) {
601
+ pushBind(syqlRangeStartBind(range.name), token, [token.span]);
602
+ pushText(' and ');
603
+ pushBind(syqlRangeEndBind(range.name), token, [token.span]);
604
+ } else {
605
+ pushBind(authored, token, [token.span]);
606
+ }
607
+ }
608
+ return parts;
609
+ }
610
+
611
+ #collectConditions(
612
+ nodes: readonly SyqlLogicalTemplateNode[],
613
+ out: SyqlLogicalWhenNode[],
614
+ ): void {
615
+ for (const node of nodes) {
616
+ if (node.kind === 'when') out.push(node);
617
+ else if (node.kind === 'predicate')
618
+ this.#collectConditions(node.body, out);
619
+ }
620
+ }
621
+
622
+ #validateControls(
623
+ query: SyqlQueryDeclaration,
624
+ conditions: readonly SyqlLogicalWhenNode[],
625
+ ): void {
626
+ const controls = new Map(query.parameters.map((item) => [item.name, item]));
627
+ for (const condition of conditions) {
628
+ condition.controls.forEach((name, index) => {
629
+ const parameter = controls.get(name);
630
+ if (parameter === undefined) {
631
+ this.#fail(
632
+ 'SYQL5008_INVALID_CONTROL',
633
+ condition.controlSpans[index] as SyqlSourceSpan,
634
+ `unknown when control ${JSON.stringify(name)}`,
635
+ );
636
+ }
637
+ if (condition.explicitPresence[index] && !parameter.optional) {
638
+ this.#fail(
639
+ 'SYQL5008_INVALID_CONTROL',
640
+ condition.controlSpans[index] as SyqlSourceSpan,
641
+ `present(${name}) requires an optional input`,
642
+ );
643
+ }
644
+ if (
645
+ !parameter.optional &&
646
+ !(
647
+ parameter.kind === 'value' &&
648
+ parameter.type?.base === 'boolean' &&
649
+ parameter.default === false
650
+ )
651
+ ) {
652
+ this.#fail(
653
+ 'SYQL5008_INVALID_CONTROL',
654
+ condition.controlSpans[index] as SyqlSourceSpan,
655
+ `required input ${name} cannot control when; use an optional input or a bool defaulted to false`,
656
+ );
657
+ }
658
+ });
659
+ }
660
+ }
661
+
662
+ #validateBindsAndDominance(
663
+ query: SyqlQueryDeclaration,
664
+ symbols: ReadonlyMap<string, QueryBindSymbol>,
665
+ template: readonly SyqlLogicalTemplateNode[],
666
+ conditions: readonly SyqlLogicalWhenNode[],
667
+ ): void {
668
+ const allUses = new Map<string, SyqlLogicalBind[]>();
669
+ const controlledUses = new Map<string, Set<string>>();
670
+ const usedControls = new Set<string>();
671
+
672
+ const record = (
673
+ bind: SyqlLogicalBind,
674
+ active: ReadonlySet<string>,
675
+ ): void => {
676
+ const symbol = symbols.get(bind.name);
677
+ if (symbol === undefined) {
678
+ this.#fail(
679
+ 'SYQL5006_UNDECLARED_BIND',
680
+ bind.span,
681
+ `bind :${bind.name} is not declared by query ${query.name}`,
682
+ );
683
+ }
684
+ const uses = allUses.get(bind.name) ?? [];
685
+ uses.push(bind);
686
+ allUses.set(bind.name, uses);
687
+ if (symbol.controller !== undefined) {
688
+ if (!active.has(symbol.controller)) {
689
+ this.#fail(
690
+ 'SYQL5009_MISSING_DOMINANCE',
691
+ bind.span,
692
+ `optional bind :${bind.name} must be inside when(${symbol.controller})`,
693
+ );
694
+ }
695
+ const names =
696
+ controlledUses.get(symbol.controller) ?? new Set<string>();
697
+ names.add(bind.name);
698
+ controlledUses.set(symbol.controller, names);
699
+ }
700
+ };
701
+
702
+ const walk = (
703
+ nodes: readonly SyqlLogicalTemplateNode[],
704
+ active: ReadonlySet<string>,
705
+ ): void => {
706
+ for (const node of nodes) {
707
+ if (node.kind === 'sql') {
708
+ for (const part of node.parts) {
709
+ if (part.kind === 'bind') record(part, active);
710
+ }
711
+ } else if (node.kind === 'predicate') {
712
+ walk(node.body, active);
713
+ } else if (node.kind === 'when') {
714
+ const next = new Set(active);
715
+ for (const control of node.controls) {
716
+ usedControls.add(control);
717
+ next.add(control);
718
+ }
719
+ walk(node.body, next);
720
+ }
721
+ }
722
+ };
723
+ walk(template, new Set());
724
+
725
+ for (const condition of conditions) {
726
+ const conditionUses = new Set<string>();
727
+ const collect = (nodes: readonly SyqlLogicalTemplateNode[]): void => {
728
+ for (const node of nodes) {
729
+ if (node.kind === 'sql') {
730
+ for (const part of node.parts) {
731
+ if (part.kind === 'bind') conditionUses.add(part.name);
732
+ }
733
+ } else if (node.kind === 'predicate') collect(node.body);
734
+ }
735
+ };
736
+ collect(condition.body);
737
+ condition.controls.forEach((control, index) => {
738
+ const parameter = query.parameters.find(
739
+ (item) => item.name === control,
740
+ );
741
+ if (
742
+ parameter?.kind === 'value' &&
743
+ parameter.default === undefined &&
744
+ !conditionUses.has(control)
745
+ ) {
746
+ this.#fail(
747
+ 'SYQL5010_UNUSED_CONTROL',
748
+ condition.controlSpans[index] as SyqlSourceSpan,
749
+ `when(${control}) does not use :${control} in its body`,
750
+ );
751
+ }
752
+ if (
753
+ parameter?.kind === 'group' &&
754
+ !parameter.members.some((member) => conditionUses.has(member.name))
755
+ ) {
756
+ this.#fail(
757
+ 'SYQL5010_UNUSED_CONTROL',
758
+ condition.controlSpans[index] as SyqlSourceSpan,
759
+ `when(${control}) does not use a member of group ${control}`,
760
+ );
761
+ }
762
+ if (
763
+ parameter?.kind === 'range' &&
764
+ !conditionUses.has(syqlRangeStartBind(parameter.name))
765
+ ) {
766
+ this.#fail(
767
+ 'SYQL5010_UNUSED_CONTROL',
768
+ condition.controlSpans[index] as SyqlSourceSpan,
769
+ `when(${control}) does not use range :${control}`,
770
+ );
771
+ }
772
+ });
773
+ }
774
+
775
+ for (const parameter of query.parameters) {
776
+ if (parameter.kind === 'range') {
777
+ if (parameter.optional && !usedControls.has(parameter.name)) {
778
+ this.#unusedInput(parameter.nameSpan, parameter.name);
779
+ }
780
+ const start = syqlRangeStartBind(parameter.name);
781
+ const end = syqlRangeEndBind(parameter.name);
782
+ if (!allUses.has(start) || !allUses.has(end)) {
783
+ this.#unusedInput(parameter.nameSpan, parameter.name);
784
+ }
785
+ } else if (parameter.kind === 'group') {
786
+ if (!usedControls.has(parameter.name)) {
787
+ this.#unusedInput(parameter.nameSpan, parameter.name);
788
+ }
789
+ const uses = controlledUses.get(parameter.name) ?? new Set<string>();
790
+ for (const member of parameter.members) {
791
+ if (!uses.has(member.name))
792
+ this.#unusedInput(member.nameSpan, member.name);
793
+ }
794
+ } else if (parameter.optional) {
795
+ if (!usedControls.has(parameter.name) || !allUses.has(parameter.name)) {
796
+ this.#unusedInput(parameter.nameSpan, parameter.name);
797
+ }
798
+ } else if (
799
+ !allUses.has(parameter.name) &&
800
+ !(parameter.default !== undefined && usedControls.has(parameter.name))
801
+ ) {
802
+ this.#unusedInput(parameter.nameSpan, parameter.name);
803
+ }
804
+ }
805
+ }
806
+
807
+ #resolveTypeRequirements(
808
+ symbols: ReadonlyMap<string, QueryBindSymbol>,
809
+ requirements: ReadonlyMap<string, readonly SyqlValueType[]>,
810
+ ): ReadonlyMap<string, SyqlValueType> {
811
+ const resolved = new Map<string, SyqlValueType>();
812
+ for (const symbol of symbols.values()) {
813
+ if (symbol.type !== undefined) resolved.set(symbol.name, symbol.type);
814
+ }
815
+ for (const [name, constraints] of requirements) {
816
+ const symbol = symbols.get(name);
817
+ if (symbol === undefined) continue;
818
+ const first = constraints[0];
819
+ if (first === undefined) continue;
820
+ for (const constraint of constraints.slice(1)) {
821
+ if (constraint.base !== first.base) {
822
+ this.#fail(
823
+ 'SYQL5011_TYPE_CONFLICT',
824
+ symbol.span,
825
+ `bind :${name} is constrained as both ${typeText(first)} and ${typeText(constraint)}`,
826
+ );
827
+ }
828
+ }
829
+ const inferred: SyqlValueType = {
830
+ base: first.base,
831
+ nullable: constraints.every((constraint) => constraint.nullable),
832
+ span: first.span,
833
+ };
834
+ if (symbol.type !== undefined) {
835
+ for (const formal of constraints) {
836
+ if (!isCompatible(symbol.type, formal)) {
837
+ this.#fail(
838
+ 'SYQL5011_TYPE_CONFLICT',
839
+ symbol.span,
840
+ `bind :${name} has type ${typeText(symbol.type)}, incompatible with predicate formal ${typeText(formal)}`,
841
+ );
842
+ }
843
+ }
844
+ resolved.set(name, symbol.type);
845
+ } else {
846
+ resolved.set(name, inferred);
847
+ }
848
+ }
849
+ return resolved;
850
+ }
851
+
852
+ #parameterType(
853
+ parameter: SyqlQueryParameter,
854
+ resolved: ReadonlyMap<string, SyqlValueType>,
855
+ ): SyqlValueType | undefined {
856
+ if (parameter.kind === 'range' || parameter.kind === 'group') {
857
+ // Group members carry their own types; the logical input itself has no
858
+ // scalar type. L3 consumes the per-member resolved map directly.
859
+ return undefined;
860
+ }
861
+ return parameter.type ?? resolved.get(parameter.name);
862
+ }
863
+
864
+ #lookupCall(
865
+ module: SyqlSyntaxFile,
866
+ name: string,
867
+ ): SyqlResolvedPredicate | undefined {
868
+ return this.#scopes.get(module.file)?.get(name);
869
+ }
870
+
871
+ #checkArity(
872
+ actual: number,
873
+ target: SyqlResolvedPredicate,
874
+ span: SyqlSourceSpan,
875
+ ): void {
876
+ const expected = target.declaration.parameters.length;
877
+ if (actual !== expected) {
878
+ this.#fail(
879
+ 'SYQL5003_PREDICATE_ARITY',
880
+ span,
881
+ `${target.declaration.name} expects ${expected} argument(s), got ${actual}`,
882
+ );
883
+ }
884
+ }
885
+
886
+ #unusedInput(span: SyqlSourceSpan, name: string): never {
887
+ this.#fail(
888
+ 'SYQL5007_UNUSED_INPUT',
889
+ span,
890
+ `query input ${name} is declared but not meaningfully used`,
891
+ );
892
+ }
893
+
894
+ #fail(
895
+ code: SyqlSemanticErrorCode,
896
+ span: SyqlSourceSpan,
897
+ message: string,
898
+ ): never {
899
+ throw new SyqlFrontendError(code, span, message);
900
+ }
901
+ }
902
+
903
+ /** Resolve and statically validate a complete reachable SYQL module graph. */
904
+ export function analyzeSyqlSemantics(
905
+ graph: SyqlModuleGraph,
906
+ ): SyqlSemanticProgram {
907
+ return new SemanticAnalyzer(graph).analyze();
908
+ }
909
+
910
+ /** Losslessly render expanded SQL/predicate nodes, retaining `when` markers. */
911
+ export function renderSyqlLogicalTemplate(
912
+ nodes: readonly SyqlLogicalTemplateNode[],
913
+ ): string {
914
+ return nodes
915
+ .map((node) => {
916
+ if (node.kind === 'sql') {
917
+ return node.parts
918
+ .map((part) => (part.kind === 'text' ? part.text : `:${part.name}`))
919
+ .join('');
920
+ }
921
+ if (node.kind === 'predicate') {
922
+ return `(${renderSyqlLogicalTemplate(node.body)})`;
923
+ }
924
+ if (node.kind === 'when') {
925
+ return `when(${node.controls.join(', ')}) {${renderSyqlLogicalTemplate(node.body)}}`;
926
+ }
927
+ throw new Error('unknown SYQL logical template node');
928
+ })
929
+ .join('');
930
+ }