xq-lsp 1.8.2 → 1.8.3-next.430
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 +16 -9
- package/builtins/builtins-fn.xq +1 -1
- package/dist/analyzer.d.ts +1 -0
- package/dist/analyzer.js +43 -3
- package/dist/ast-nodes.d.ts +38 -0
- package/dist/ast-nodes.js +71 -0
- package/dist/diagnostics.js +2 -0
- package/dist/functioncall-diagnostics.d.ts +6 -3
- package/dist/functioncall-diagnostics.js +76 -17
- package/dist/inlay-hints.js +28 -82
- package/dist/module-siblings.d.ts +11 -0
- package/dist/module-siblings.js +16 -0
- package/dist/predeclared-namespaces.js +1 -0
- package/dist/prolog-diagnostics.d.ts +10 -0
- package/dist/prolog-diagnostics.js +113 -0
- package/dist/qt4-worker.js +5 -3
- package/dist/runtimes/fonto/fonto.xq +95 -0
- package/dist/semantic-tokens.d.ts +29 -0
- package/dist/semantic-tokens.js +254 -0
- package/dist/server.js +40 -7
- package/dist/typechecker.d.ts +48 -1
- package/dist/typechecker.js +1215 -122
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
- package/src/analyzer.ts +45 -3
- package/src/ast-nodes.ts +85 -0
- package/src/diagnostics.ts +2 -0
- package/src/functioncall-diagnostics.ts +93 -15
- package/src/inlay-hints.ts +42 -93
- package/src/module-siblings.ts +17 -0
- package/src/predeclared-namespaces.ts +1 -0
- package/src/prolog-diagnostics.ts +137 -0
- package/src/qt4-worker.ts +6 -4
- package/src/runtimes/fonto/fonto.xq +95 -0
- package/src/semantic-tokens.ts +289 -0
- package/src/server.ts +40 -6
- package/src/typechecker.ts +1265 -123
- package/src/types.ts +2 -0
package/dist/typechecker.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { formatQName, qnameKey } from "./types.js";
|
|
2
|
-
import { findAll, isTerminal, directChildOf } from "./analyzer.js";
|
|
3
|
-
import { asFunctionCall, asVarRef,
|
|
2
|
+
import { findAll, isTerminal, directChildOf, directChildrenOf, firstTerminalValue, parseEQName, resolvePrefix, XMLNS_ARRAY, XMLNS_FN, XMLNS_MAP, } from "./analyzer.js";
|
|
3
|
+
import { asArithmeticExpr, asFunctionCall, asNamedFunctionRef, asVarRef, asVarName, literalKind, isPathExpr, argExpr } from "./ast-nodes.js";
|
|
4
4
|
// ── Type constants ───────────────────────────────────────────────────────────
|
|
5
5
|
const UNKNOWN = { kind: "unknown", occurrence: "" };
|
|
6
6
|
// ── Type parsing ─────────────────────────────────────────────────────────────
|
|
@@ -174,15 +174,146 @@ export function isAssignable(from, to) {
|
|
|
174
174
|
return isNodeSubtype(from.name, to.name);
|
|
175
175
|
return true;
|
|
176
176
|
}
|
|
177
|
+
// ── Type algebra ──────────────────────────────────────────────────────────────
|
|
178
|
+
const EMPTY = { kind: "empty", occurrence: "" };
|
|
179
|
+
const BOOLEAN = { kind: "atomic", name: "xs:boolean", occurrence: "" };
|
|
180
|
+
const STRING = { kind: "atomic", name: "xs:string", occurrence: "" };
|
|
181
|
+
const INTEGER = { kind: "atomic", name: "xs:integer", occurrence: "" };
|
|
182
|
+
const ONE = { min: 1, max: 1 };
|
|
183
|
+
function cardOf(t) {
|
|
184
|
+
if (t.kind === "empty")
|
|
185
|
+
return { min: 0, max: 0 };
|
|
186
|
+
if (t.kind === "unknown")
|
|
187
|
+
return { min: 0, max: 2 };
|
|
188
|
+
switch (t.occurrence) {
|
|
189
|
+
case "?":
|
|
190
|
+
return { min: 0, max: 1 };
|
|
191
|
+
case "+":
|
|
192
|
+
return { min: 1, max: 2 };
|
|
193
|
+
case "*":
|
|
194
|
+
return { min: 0, max: 2 };
|
|
195
|
+
default:
|
|
196
|
+
return ONE;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function withCard(t, c) {
|
|
200
|
+
if (t.kind === "unknown")
|
|
201
|
+
return t;
|
|
202
|
+
if (c.max === 0 || t.kind === "empty")
|
|
203
|
+
return EMPTY;
|
|
204
|
+
const occurrence = c.min === 1 ? (c.max === 1 ? "" : "+") : c.max === 1 ? "?" : "*";
|
|
205
|
+
return { ...t, occurrence };
|
|
206
|
+
}
|
|
207
|
+
/** Cardinality of evaluating `b` once per item of `a` (FLWOR `for`, `!`, path steps). */
|
|
208
|
+
function multiplyCard(a, b) {
|
|
209
|
+
const min = a.min === 1 && b.min === 1 ? 1 : 0;
|
|
210
|
+
const max = a.max === 0 || b.max === 0 ? 0 : a.max === 1 && b.max === 1 ? 1 : 2;
|
|
211
|
+
return { min, max };
|
|
212
|
+
}
|
|
213
|
+
/** The type of a single item of `t` — e.g. what a `for` variable over `t` holds. */
|
|
214
|
+
export function itemTypeOf(t) {
|
|
215
|
+
if (t.kind === "unknown" || t.kind === "empty")
|
|
216
|
+
return t;
|
|
217
|
+
return t.occurrence === "" ? t : { ...t, occurrence: "" };
|
|
218
|
+
}
|
|
219
|
+
function derivesFrom(from, to) {
|
|
220
|
+
for (let cur = from; cur !== undefined; cur = ATOMIC_TYPE_PARENT[cur]) {
|
|
221
|
+
if (cur === to)
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
// Nearest common supertype in the derivation hierarchy (no promotion), falling back to
|
|
227
|
+
// xs:numeric for mixed numerics (e.g. `if (…) then 1 else 1e0`) and xs:anyAtomicType otherwise.
|
|
228
|
+
function commonAtomicSupertype(a, b) {
|
|
229
|
+
for (let cur = a; cur !== undefined && cur !== "xs:anyAtomicType"; cur = ATOMIC_TYPE_PARENT[cur]) {
|
|
230
|
+
if (derivesFrom(b, cur))
|
|
231
|
+
return cur;
|
|
232
|
+
}
|
|
233
|
+
if (isAtomicSubtype(a, "xs:numeric") && isAtomicSubtype(b, "xs:numeric"))
|
|
234
|
+
return "xs:numeric";
|
|
235
|
+
return "xs:anyAtomicType";
|
|
236
|
+
}
|
|
237
|
+
// Least upper bound of two (non-empty, known) item types, ignoring occurrence.
|
|
238
|
+
function itemUnion(a, b) {
|
|
239
|
+
if (a.kind === b.kind) {
|
|
240
|
+
switch (a.kind) {
|
|
241
|
+
case "atomic":
|
|
242
|
+
return { kind: "atomic", name: commonAtomicSupertype(a.name ?? "", b.name ?? ""), occurrence: "" };
|
|
243
|
+
case "node":
|
|
244
|
+
return { kind: "node", name: a.name === b.name ? a.name : "node", occurrence: "" };
|
|
245
|
+
case "map":
|
|
246
|
+
case "array":
|
|
247
|
+
case "function":
|
|
248
|
+
return { kind: a.kind, name: a.name === b.name ? a.name : `${a.kind}(*)`, occurrence: "" };
|
|
249
|
+
default:
|
|
250
|
+
return { kind: a.kind, occurrence: "" };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { kind: "item", occurrence: "" };
|
|
254
|
+
}
|
|
255
|
+
/** Type of an expression that yields either `a` or `b` (if/else, switch, typeswitch, try/catch). */
|
|
256
|
+
export function choiceType(a, b) {
|
|
257
|
+
if (a.kind === "unknown" || b.kind === "unknown")
|
|
258
|
+
return UNKNOWN;
|
|
259
|
+
if (a.kind === "empty" && b.kind === "empty")
|
|
260
|
+
return EMPTY;
|
|
261
|
+
const ca = cardOf(a);
|
|
262
|
+
const cb = cardOf(b);
|
|
263
|
+
const card = { min: ca.min === 1 && cb.min === 1 ? 1 : 0, max: ca.max > cb.max ? ca.max : cb.max };
|
|
264
|
+
if (a.kind === "empty")
|
|
265
|
+
return withCard(b, card);
|
|
266
|
+
if (b.kind === "empty")
|
|
267
|
+
return withCard(a, card);
|
|
268
|
+
return withCard(itemUnion(a, b), card);
|
|
269
|
+
}
|
|
270
|
+
/** Type of the sequence `(a, b)`. */
|
|
271
|
+
export function concatType(a, b) {
|
|
272
|
+
if (a.kind === "unknown" || b.kind === "unknown")
|
|
273
|
+
return UNKNOWN;
|
|
274
|
+
if (a.kind === "empty")
|
|
275
|
+
return b;
|
|
276
|
+
if (b.kind === "empty")
|
|
277
|
+
return a;
|
|
278
|
+
const ca = cardOf(a);
|
|
279
|
+
const cb = cardOf(b);
|
|
280
|
+
return withCard(itemUnion(a, b), { min: 1, max: ca.max + cb.max > 1 ? 2 : 1 });
|
|
281
|
+
}
|
|
282
|
+
// Splits the parameter list of a parameterized type name, e.g. "map(xs:string, item()*)"
|
|
283
|
+
// → ["xs:string", "item()*"], respecting nested parentheses.
|
|
284
|
+
function typeArgs(name) {
|
|
285
|
+
const open = name.indexOf("(");
|
|
286
|
+
const inner = name.slice(open + 1, name.lastIndexOf(")"));
|
|
287
|
+
const args = [];
|
|
288
|
+
let depth = 0;
|
|
289
|
+
let startIdx = 0;
|
|
290
|
+
for (let i = 0; i < inner.length; i++) {
|
|
291
|
+
if (inner[i] === "(")
|
|
292
|
+
depth++;
|
|
293
|
+
else if (inner[i] === ")")
|
|
294
|
+
depth--;
|
|
295
|
+
else if (inner[i] === "," && depth === 0) {
|
|
296
|
+
args.push(inner.slice(startIdx, i).trim());
|
|
297
|
+
startIdx = i + 1;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
args.push(inner.slice(startIdx).trim());
|
|
301
|
+
return args.filter((a) => a !== "");
|
|
302
|
+
}
|
|
303
|
+
function typeText(t, fallback) {
|
|
304
|
+
return t.kind === "unknown" ? fallback : formatType(t);
|
|
305
|
+
}
|
|
177
306
|
// ── Type inference ────────────────────────────────────────────────────────────
|
|
178
307
|
const LITERAL_TYPE = {
|
|
179
|
-
string:
|
|
180
|
-
integer:
|
|
308
|
+
string: STRING,
|
|
309
|
+
integer: INTEGER,
|
|
181
310
|
decimal: { kind: "atomic", name: "xs:decimal", occurrence: "" },
|
|
182
311
|
double: { kind: "atomic", name: "xs:double", occurrence: "" },
|
|
183
312
|
};
|
|
184
313
|
const NODE_STEP = { kind: "node", name: "node", occurrence: "*" };
|
|
185
314
|
const RANGE_SEQUENCE = { kind: "atomic", name: "xs:integer", occurrence: "*" };
|
|
315
|
+
/** Scope key under which the context item's type (`.`) is tracked, e.g. inside `!` and predicates. */
|
|
316
|
+
const CONTEXT_ITEM_KEY = ".";
|
|
186
317
|
// Reuses the existing xs:numeric union check and subtype/promotion chain (isAtomicSubtype)
|
|
187
318
|
// rather than a separate hardcoded numeric ordering.
|
|
188
319
|
function numericAtomicName(t) {
|
|
@@ -195,18 +326,79 @@ function widenNumeric(a, b) {
|
|
|
195
326
|
return a;
|
|
196
327
|
return "xs:double"; // neither promotes to the other — fall back to the widest common type
|
|
197
328
|
}
|
|
198
|
-
|
|
329
|
+
const UNTYPED_ATOMIC = "xs:untypedAtomic";
|
|
330
|
+
// Atomized item type of `t`, keeping its cardinality. Nodes are assumed untyped (not
|
|
331
|
+
// schema-validated), so element/attribute/text/document values atomize to xs:untypedAtomic;
|
|
332
|
+
// comments and processing instructions atomize to xs:string.
|
|
333
|
+
function atomize(t) {
|
|
334
|
+
if (t.kind === "atomic" || t.kind === "empty")
|
|
335
|
+
return t;
|
|
336
|
+
if (t.kind === "node") {
|
|
337
|
+
const name = t.name === "comment" || t.name === "processing-instruction" ? "xs:string" : UNTYPED_ATOMIC;
|
|
338
|
+
return { kind: "atomic", name, occurrence: t.occurrence };
|
|
339
|
+
}
|
|
340
|
+
return UNKNOWN;
|
|
341
|
+
}
|
|
342
|
+
/** Arithmetic and aggregate functions cast xs:untypedAtomic operands to xs:double. */
|
|
343
|
+
function arithmeticOperandName(t) {
|
|
344
|
+
return t.name === UNTYPED_ATOMIC ? "xs:double" : t.name;
|
|
345
|
+
}
|
|
346
|
+
const YEAR_MONTH = "xs:yearMonthDuration";
|
|
347
|
+
const DAY_TIME = "xs:dayTimeDuration";
|
|
348
|
+
const isDuration = (n) => n === YEAR_MONTH || n === DAY_TIME;
|
|
349
|
+
const isDateOrDateTime = (n) => n === "xs:date" || n === "xs:dateTime";
|
|
350
|
+
// Item type of `a op b` for single atomic operands, per the XPath 3.1 operator mapping table
|
|
351
|
+
// (numeric arithmetic, date/time subtraction, date/time ± duration, duration scaling).
|
|
352
|
+
function arithmeticItemName(op, a, b) {
|
|
353
|
+
const aNum = isAtomicSubtype(a, "xs:numeric");
|
|
354
|
+
const bNum = isAtomicSubtype(b, "xs:numeric");
|
|
355
|
+
if (aNum && bNum) {
|
|
356
|
+
if (op === "idiv")
|
|
357
|
+
return "xs:integer";
|
|
358
|
+
if (op === "div" && isAtomicSubtype(a, "xs:decimal") && isAtomicSubtype(b, "xs:decimal")) {
|
|
359
|
+
return widenNumeric(widenNumeric(a, b), "xs:decimal"); // integer div integer is never exact
|
|
360
|
+
}
|
|
361
|
+
return widenNumeric(a, b);
|
|
362
|
+
}
|
|
363
|
+
const additive = op === "+" || op === "-";
|
|
364
|
+
if (op === "-" && a === b && (isDateOrDateTime(a) || a === "xs:time"))
|
|
365
|
+
return DAY_TIME;
|
|
366
|
+
if (additive && isDateOrDateTime(a) && isDuration(b))
|
|
367
|
+
return a;
|
|
368
|
+
if (additive && a === "xs:time" && b === DAY_TIME)
|
|
369
|
+
return a;
|
|
370
|
+
if (op === "+" && isDuration(a) && (isDateOrDateTime(b) || (b === "xs:time" && a === DAY_TIME)))
|
|
371
|
+
return b;
|
|
372
|
+
if (additive && isDuration(a) && a === b)
|
|
373
|
+
return a;
|
|
374
|
+
if ((op === "*" || op === "div") && isDuration(a) && bNum)
|
|
375
|
+
return a;
|
|
376
|
+
if (op === "*" && aNum && isDuration(b))
|
|
377
|
+
return b;
|
|
378
|
+
if (op === "div" && isDuration(a) && a === b)
|
|
379
|
+
return "xs:decimal";
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
// Result type of a `+`/`-`/`*`/`div`/`idiv`/`mod` operator: operands are atomized, an empty
|
|
383
|
+
// operand gives (), an optional one makes the result optional, and more than one item is a
|
|
384
|
+
// type error (so we don't guess).
|
|
199
385
|
function arithmeticResult(op, a, b) {
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
386
|
+
const ta = atomize(a);
|
|
387
|
+
const tb = atomize(b);
|
|
388
|
+
if (ta.kind === "unknown" || tb.kind === "unknown")
|
|
203
389
|
return UNKNOWN;
|
|
204
|
-
if (
|
|
205
|
-
return
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
390
|
+
if (ta.kind === "empty" || tb.kind === "empty")
|
|
391
|
+
return EMPTY;
|
|
392
|
+
const ca = cardOf(ta);
|
|
393
|
+
const cb = cardOf(tb);
|
|
394
|
+
if (ca.max > 1 || cb.max > 1)
|
|
395
|
+
return UNKNOWN;
|
|
396
|
+
const aName = arithmeticOperandName(ta);
|
|
397
|
+
const bName = arithmeticOperandName(tb);
|
|
398
|
+
const name = aName && bName ? arithmeticItemName(op, aName, bName) : null;
|
|
399
|
+
if (!name)
|
|
400
|
+
return UNKNOWN;
|
|
401
|
+
return { kind: "atomic", name, occurrence: ca.min === 1 && cb.min === 1 ? "" : "?" };
|
|
210
402
|
}
|
|
211
403
|
function allFunctionsFlat(analysis, importedAnalyses) {
|
|
212
404
|
const fns = [...analysis.functions];
|
|
@@ -214,15 +406,677 @@ function allFunctionsFlat(analysis, importedAnalyses) {
|
|
|
214
406
|
fns.push(...a.functions);
|
|
215
407
|
return fns;
|
|
216
408
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
409
|
+
/** Finds the declaration a call with `argCount` arguments resolves to, honouring variadic functions. */
|
|
410
|
+
export function resolveFunction(qname, argCount, allFns) {
|
|
411
|
+
return allFns.find((f) => f.qname.namespaceUri === qname.namespaceUri &&
|
|
412
|
+
f.qname.localName === qname.localName &&
|
|
413
|
+
(f.variadic ? argCount >= f.arity : f.arity === argCount));
|
|
414
|
+
}
|
|
415
|
+
/** Concatenated terminal text of a node — enough to reconstruct a SequenceType for `parseType`. */
|
|
416
|
+
function nodeText(node) {
|
|
417
|
+
if (isTerminal(node))
|
|
418
|
+
return node.value;
|
|
419
|
+
return node.children.map(nodeText).join("");
|
|
420
|
+
}
|
|
421
|
+
function sequenceTypeOf(node) {
|
|
422
|
+
if (!node)
|
|
423
|
+
return undefined;
|
|
424
|
+
const text = nodeText(node);
|
|
425
|
+
// Typeswitch case unions (`case xs:integer | xs:string`) are a choice of their members.
|
|
426
|
+
if (text.includes("|"))
|
|
427
|
+
return text.split("|").map(parseType).reduce(choiceType);
|
|
428
|
+
return parseType(text);
|
|
429
|
+
}
|
|
430
|
+
/** The declared `as SequenceType` of a binding, parameter or variable declaration, if any. */
|
|
431
|
+
function declaredTypeOf(node) {
|
|
432
|
+
const typeDecl = directChildOf(node, "TypeDeclaration");
|
|
433
|
+
return typeDecl ? sequenceTypeOf(directChildOf(typeDecl, "SequenceType")) : undefined;
|
|
434
|
+
}
|
|
435
|
+
// Generic built-ins whose declared signature (item()*) loses the argument's item type.
|
|
436
|
+
// Maps local name → the cardinality of the result relative to the first argument's item type.
|
|
437
|
+
const FN_SEQUENCE_PRESERVING = {
|
|
438
|
+
head: { min: 0, max: 1 },
|
|
439
|
+
"exactly-one": ONE,
|
|
440
|
+
"zero-or-one": { min: 0, max: 1 },
|
|
441
|
+
"one-or-more": { min: 1, max: 2 },
|
|
442
|
+
tail: { min: 0, max: 2 },
|
|
443
|
+
subsequence: { min: 0, max: 2 },
|
|
444
|
+
remove: { min: 0, max: 2 },
|
|
445
|
+
filter: { min: 0, max: 2 },
|
|
446
|
+
reverse: "same",
|
|
447
|
+
unordered: "same",
|
|
448
|
+
sort: "same",
|
|
449
|
+
foot: { min: 0, max: 1 },
|
|
450
|
+
trunk: { min: 0, max: 2 },
|
|
451
|
+
};
|
|
452
|
+
/** `R` of a single `function(…) as R` item, if known. */
|
|
453
|
+
function functionResultType(f) {
|
|
454
|
+
if (f.kind !== "function" || f.occurrence !== "" || !f.name?.includes(" as "))
|
|
455
|
+
return undefined;
|
|
456
|
+
const r = parseType(f.name.slice(f.name.lastIndexOf(" as ") + 4));
|
|
457
|
+
return r.kind === "unknown" ? undefined : r;
|
|
458
|
+
}
|
|
459
|
+
/** Member types of a single map(K, V) / array(T): `[K, V]` or `[T]`, or undefined for `map(*)` etc. */
|
|
460
|
+
function containerArgs(t, kind) {
|
|
461
|
+
if (t.kind !== kind || t.occurrence !== "" || !t.name)
|
|
462
|
+
return undefined;
|
|
463
|
+
const args = typeArgs(t.name).map(parseType);
|
|
464
|
+
if (args.length !== (kind === "map" ? 2 : 1) || args.some((a) => a.kind === "unknown"))
|
|
465
|
+
return undefined;
|
|
466
|
+
return args;
|
|
467
|
+
}
|
|
468
|
+
// Aggregates (sum/avg/min/max): the atomized item name with untypedAtomic cast to xs:double,
|
|
469
|
+
// or undefined when it isn't numeric or a duration (or, for min/max, another comparable type).
|
|
470
|
+
function aggregateItemName(t, allowAnyAtomic) {
|
|
471
|
+
const atomized = atomize(t);
|
|
472
|
+
if (atomized.kind !== "atomic")
|
|
473
|
+
return undefined;
|
|
474
|
+
const name = arithmeticOperandName(atomized);
|
|
475
|
+
if (!name)
|
|
476
|
+
return undefined;
|
|
477
|
+
if (isAtomicSubtype(name, "xs:numeric") || isDuration(name) || allowAnyAtomic)
|
|
478
|
+
return name;
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
// Built-ins whose declared signature (item()*, xs:anyAtomicType*, …) loses information the
|
|
482
|
+
// arguments carry: aggregates, atomization, higher-order functions, and map/array accessors.
|
|
483
|
+
function inferPolymorphicBuiltin(ns, name, args) {
|
|
484
|
+
const [first, second, third] = args;
|
|
485
|
+
if (!first || first.kind === "unknown")
|
|
486
|
+
return undefined;
|
|
487
|
+
const card = cardOf(first);
|
|
488
|
+
if (ns === XMLNS_FN) {
|
|
489
|
+
if (name in FN_SEQUENCE_PRESERVING) {
|
|
490
|
+
const rule = FN_SEQUENCE_PRESERVING[name];
|
|
491
|
+
return rule === "same" ? first : withCard(first, rule);
|
|
492
|
+
}
|
|
493
|
+
switch (name) {
|
|
494
|
+
case "data":
|
|
495
|
+
return args.length === 1 ? atomize(first) : undefined;
|
|
496
|
+
case "distinct-values": {
|
|
497
|
+
const atomized = atomize(first);
|
|
498
|
+
return atomized.kind === "unknown" ? undefined : withCard(atomized, { min: card.min, max: card.max });
|
|
499
|
+
}
|
|
500
|
+
case "sum": {
|
|
501
|
+
if (first.kind === "empty")
|
|
502
|
+
return args.length === 1 ? INTEGER : second;
|
|
503
|
+
const item = aggregateItemName(first, false);
|
|
504
|
+
if (!item)
|
|
505
|
+
return undefined;
|
|
506
|
+
const t = { kind: "atomic", name: item, occurrence: "" };
|
|
507
|
+
// sum(()) is 0, or the explicit zero value when one is given.
|
|
508
|
+
return card.min === 1 || args.length === 1 ? t : second && choiceType(t, second);
|
|
509
|
+
}
|
|
510
|
+
case "avg":
|
|
511
|
+
case "min":
|
|
512
|
+
case "max": {
|
|
513
|
+
if (first.kind === "empty")
|
|
514
|
+
return EMPTY;
|
|
515
|
+
const item = aggregateItemName(first, name !== "avg");
|
|
516
|
+
if (!item)
|
|
517
|
+
return undefined;
|
|
518
|
+
const avgName = isAtomicSubtype(item, "xs:decimal") ? "xs:decimal" : item;
|
|
519
|
+
return { kind: "atomic", name: name === "avg" ? avgName : item, occurrence: card.min === 1 ? "" : "?" };
|
|
520
|
+
}
|
|
521
|
+
case "for-each": {
|
|
522
|
+
const r = second && functionResultType(second);
|
|
523
|
+
return r && withCard(r, multiplyCard(card, cardOf(r)));
|
|
524
|
+
}
|
|
525
|
+
case "for-each-pair": {
|
|
526
|
+
const r = third && functionResultType(third);
|
|
527
|
+
if (!r || !second || second.kind === "unknown")
|
|
528
|
+
return undefined;
|
|
529
|
+
const other = cardOf(second);
|
|
530
|
+
const pairs = { min: card.min === 1 && other.min === 1 ? 1 : 0, max: card.max < other.max ? card.max : other.max };
|
|
531
|
+
return withCard(r, multiplyCard(pairs, cardOf(r)));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
if (ns === XMLNS_MAP) {
|
|
537
|
+
switch (name) {
|
|
538
|
+
case "entry": {
|
|
539
|
+
const key = atomize(first);
|
|
540
|
+
if (key.kind !== "atomic" || !second || second.kind === "unknown")
|
|
541
|
+
return undefined;
|
|
542
|
+
return { kind: "map", name: `map(${formatType(itemTypeOf(key))}, ${formatType(second)})`, occurrence: "" };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const kv = containerArgs(first, "map");
|
|
546
|
+
if (!kv)
|
|
547
|
+
return undefined;
|
|
548
|
+
const [key, value] = kv;
|
|
549
|
+
switch (name) {
|
|
550
|
+
case "get":
|
|
551
|
+
return withCard(value, { min: 0, max: cardOf(value).max });
|
|
552
|
+
case "keys":
|
|
553
|
+
return withCard(key, { min: 0, max: 2 });
|
|
554
|
+
case "remove":
|
|
555
|
+
return first;
|
|
556
|
+
case "put": {
|
|
557
|
+
if (!second || !third || second.kind === "unknown" || third.kind === "unknown")
|
|
558
|
+
return undefined;
|
|
559
|
+
const newKey = atomize(itemTypeOf(second));
|
|
560
|
+
if (newKey.kind !== "atomic")
|
|
561
|
+
return undefined;
|
|
562
|
+
return { kind: "map", name: `map(${formatType(itemUnion(key, newKey))}, ${formatType(choiceType(value, third))})`, occurrence: "" };
|
|
563
|
+
}
|
|
564
|
+
case "for-each": {
|
|
565
|
+
const r = second && functionResultType(second);
|
|
566
|
+
return r && withCard(r, multiplyCard({ min: 0, max: 2 }, cardOf(r)));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
if (ns === XMLNS_ARRAY) {
|
|
572
|
+
const members = containerArgs(first, "array");
|
|
573
|
+
if (!members)
|
|
574
|
+
return undefined;
|
|
575
|
+
const [member] = members;
|
|
576
|
+
switch (name) {
|
|
577
|
+
case "get":
|
|
578
|
+
case "head":
|
|
579
|
+
case "foot":
|
|
580
|
+
return member;
|
|
581
|
+
case "tail":
|
|
582
|
+
case "trunk":
|
|
583
|
+
case "reverse":
|
|
584
|
+
case "subarray":
|
|
585
|
+
case "remove":
|
|
586
|
+
case "filter":
|
|
587
|
+
case "sort":
|
|
588
|
+
return first;
|
|
589
|
+
case "append":
|
|
590
|
+
return second && second.kind !== "unknown" ? { kind: "array", name: `array(${formatType(choiceType(member, second))})`, occurrence: "" } : undefined;
|
|
591
|
+
case "for-each": {
|
|
592
|
+
const r = second && functionResultType(second);
|
|
593
|
+
return r && { kind: "array", name: `array(${formatType(r)})`, occurrence: "" };
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return undefined;
|
|
598
|
+
}
|
|
599
|
+
function inferCallResult(qname, argTypes, argCount, allFns) {
|
|
600
|
+
if (argCount >= 1 && (qname.namespaceUri === XMLNS_FN || qname.namespaceUri === XMLNS_MAP || qname.namespaceUri === XMLNS_ARRAY)) {
|
|
601
|
+
const special = inferPolymorphicBuiltin(qname.namespaceUri, qname.localName, argTypes());
|
|
602
|
+
if (special)
|
|
603
|
+
return special;
|
|
604
|
+
}
|
|
605
|
+
const fn = resolveFunction(qname, argCount, allFns);
|
|
224
606
|
return fn?.returnType ? parseType(fn.returnType) : UNKNOWN;
|
|
225
607
|
}
|
|
608
|
+
function functionTypeOf(fn) {
|
|
609
|
+
const params = fn.params.map((p) => p.type ?? "item()*").join(", ");
|
|
610
|
+
return { kind: "function", name: `function(${params}) as ${fn.returnType ?? "item()*"}`, occurrence: "" };
|
|
611
|
+
}
|
|
612
|
+
// Kind of node selected by an axis step: `@x`/`attribute::x` → attribute, a plain name test on
|
|
613
|
+
// any other axis → element, a kind test → that kind, `..` → any node.
|
|
614
|
+
function axisStepType(step) {
|
|
615
|
+
const text = nodeText(step);
|
|
616
|
+
if (text.startsWith(".."))
|
|
617
|
+
return NODE_STEP;
|
|
618
|
+
const kindTests = findAll(step, "KindTest");
|
|
619
|
+
if (kindTests.length > 0) {
|
|
620
|
+
const kind = nodeText(kindTests[0]).replace(/\(.*$/s, "");
|
|
621
|
+
const name = kind === "schema-element" ? "element" : kind === "schema-attribute" ? "attribute" : kind;
|
|
622
|
+
return { kind: "node", name, occurrence: "*" };
|
|
623
|
+
}
|
|
624
|
+
const isAttributeAxis = text.startsWith("@") || text.startsWith("attribute::");
|
|
625
|
+
return { kind: "node", name: isAttributeAxis ? "attribute" : "element", occurrence: "*" };
|
|
626
|
+
}
|
|
627
|
+
function inferPathExpr(nt, varTypes, analysis, allFns) {
|
|
628
|
+
const steps = nt.children.filter((c) => !isTerminal(c));
|
|
629
|
+
if (steps.length === 0)
|
|
630
|
+
return { kind: "node", name: "document-node", occurrence: "" }; // lone "/"
|
|
631
|
+
// Every step before the last feeds the path's navigation — if one provably isn't a node
|
|
632
|
+
// (a literal, an atomic function result, …) the path is a type error; don't guess.
|
|
633
|
+
const definitelyNotNodes = steps.slice(0, -1).some((step) => {
|
|
634
|
+
const stepType = inferExprType(step, varTypes, analysis, allFns);
|
|
635
|
+
return stepType.kind !== "unknown" && stepType.kind !== "node" && stepType.kind !== "item";
|
|
636
|
+
});
|
|
637
|
+
if (definitelyNotNodes)
|
|
638
|
+
return UNKNOWN;
|
|
639
|
+
const last = inferExprType(steps[steps.length - 1], varTypes, analysis, allFns);
|
|
640
|
+
if (last.kind === "node" || last.kind === "atomic")
|
|
641
|
+
return withCard(last, { min: 0, max: 2 });
|
|
642
|
+
return NODE_STEP;
|
|
643
|
+
}
|
|
644
|
+
function inferPostfixExpr(nt, varTypes, analysis, allFns) {
|
|
645
|
+
const [base, ...postfixes] = nt.children.filter((c) => !isTerminal(c));
|
|
646
|
+
let t = inferExprType(base, varTypes, analysis, allFns);
|
|
647
|
+
for (const postfix of postfixes) {
|
|
648
|
+
if (t.kind === "unknown")
|
|
649
|
+
return UNKNOWN;
|
|
650
|
+
const card = cardOf(t);
|
|
651
|
+
if (postfix.type === "Predicate") {
|
|
652
|
+
const expr = directChildOf(postfix, "Expr");
|
|
653
|
+
const predScope = new Map(varTypes);
|
|
654
|
+
predScope.set(CONTEXT_ITEM_KEY, itemTypeOf(t));
|
|
655
|
+
const predType = expr ? inferExprType(expr, predScope, analysis, allFns) : UNKNOWN;
|
|
656
|
+
// A single numeric predicate (`$seq[1]`, `$seq[$i]`) selects at most one item.
|
|
657
|
+
const positional = predType.occurrence === "" && numericAtomicName(predType) !== null;
|
|
658
|
+
t = withCard(t, { min: 0, max: positional && card.max > 1 ? 1 : card.max });
|
|
659
|
+
}
|
|
660
|
+
else if (postfix.type === "Lookup") {
|
|
661
|
+
t = lookupType(t, postfix);
|
|
662
|
+
}
|
|
663
|
+
else if (postfix.type === "ArgumentList" && t.kind === "function" && t.occurrence === "" && t.name?.includes(" as ")) {
|
|
664
|
+
t = parseType(t.name.slice(t.name.lastIndexOf(" as ") + 4));
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
return UNKNOWN;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return t;
|
|
671
|
+
}
|
|
672
|
+
// `$m?key` / `$a?1` / `$x?*` on a map(K, V) or array(T).
|
|
673
|
+
function lookupType(t, lookup) {
|
|
674
|
+
if ((t.kind !== "map" && t.kind !== "array") || !t.name)
|
|
675
|
+
return UNKNOWN;
|
|
676
|
+
const args = typeArgs(t.name);
|
|
677
|
+
const memberText = t.kind === "map" ? args[1] : args[0];
|
|
678
|
+
if (!memberText || memberText === "*")
|
|
679
|
+
return UNKNOWN;
|
|
680
|
+
const member = parseType(memberText);
|
|
681
|
+
if (member.kind === "unknown")
|
|
682
|
+
return UNKNOWN;
|
|
683
|
+
const wildcard = nodeText(lookup).endsWith("*");
|
|
684
|
+
// A map lookup of a missing key yields (); an array lookup out of bounds is an error.
|
|
685
|
+
const perLookup = wildcard ? { min: 0, max: 2 } : t.kind === "map" ? { min: 0, max: cardOf(member).max } : cardOf(member);
|
|
686
|
+
const each = wildcard || t.kind === "map" ? multiplyCard(perLookup, cardOf(member)) : perLookup;
|
|
687
|
+
return withCard(member, multiplyCard(cardOf(t), each));
|
|
688
|
+
}
|
|
689
|
+
function inferMapConstructor(nt, varTypes, analysis, allFns) {
|
|
690
|
+
const entries = directChildrenOf(nt, "MapConstructorEntry");
|
|
691
|
+
if (entries.length === 0)
|
|
692
|
+
return { kind: "map", name: "map(*)", occurrence: "" };
|
|
693
|
+
let key;
|
|
694
|
+
let value;
|
|
695
|
+
for (const entry of entries) {
|
|
696
|
+
const k = directChildOf(entry, "MapKeyExpr");
|
|
697
|
+
const v = directChildOf(entry, "MapValueExpr");
|
|
698
|
+
const kt = k ? itemTypeOf(inferExprType(k, varTypes, analysis, allFns)) : UNKNOWN;
|
|
699
|
+
const vt = v ? inferExprType(v, varTypes, analysis, allFns) : UNKNOWN;
|
|
700
|
+
key = key === undefined ? kt : kt.kind === "unknown" || key.kind === "unknown" ? UNKNOWN : itemUnion(key, kt);
|
|
701
|
+
value = value === undefined ? vt : choiceType(value, vt);
|
|
702
|
+
}
|
|
703
|
+
const keyText = key && key.kind === "atomic" ? formatType(key) : "xs:anyAtomicType";
|
|
704
|
+
return { kind: "map", name: `map(${keyText}, ${typeText(value ?? UNKNOWN, "item()*")})`, occurrence: "" };
|
|
705
|
+
}
|
|
706
|
+
function inferArrayConstructor(nt, varTypes, analysis, allFns) {
|
|
707
|
+
let member;
|
|
708
|
+
if (nt.type === "SquareArrayConstructor") {
|
|
709
|
+
for (const m of directChildrenOf(nt, "ExprSingle")) {
|
|
710
|
+
const mt = inferExprType(m, varTypes, analysis, allFns);
|
|
711
|
+
member = member === undefined ? mt : choiceType(member, mt);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
// array { E } has one member per item of E.
|
|
716
|
+
const enclosed = directChildOf(nt, "EnclosedExpr");
|
|
717
|
+
const et = enclosed ? inferExprType(enclosed, varTypes, analysis, allFns) : EMPTY;
|
|
718
|
+
if (et.kind !== "empty")
|
|
719
|
+
member = itemTypeOf(et);
|
|
720
|
+
}
|
|
721
|
+
if (member === undefined || member.kind === "empty")
|
|
722
|
+
return { kind: "array", name: "array(*)", occurrence: "" };
|
|
723
|
+
return { kind: "array", name: `array(${typeText(member, "item()*")})`, occurrence: "" };
|
|
724
|
+
}
|
|
725
|
+
function inferArrowExpr(nt, varTypes, analysis, allFns) {
|
|
726
|
+
let t;
|
|
727
|
+
let op;
|
|
728
|
+
for (let i = 0; i < nt.children.length; i++) {
|
|
729
|
+
const c = nt.children[i];
|
|
730
|
+
if (t === undefined) {
|
|
731
|
+
t = inferExprType(c, varTypes, analysis, allFns);
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (isTerminal(c)) {
|
|
735
|
+
op = c.value;
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (c.type !== "ArrowFunctionSpecifier")
|
|
739
|
+
continue;
|
|
740
|
+
const eqname = directChildOf(c, "EQName");
|
|
741
|
+
const argList = nt.children[i + 1];
|
|
742
|
+
if (op !== "=>" || !eqname || argList?.type !== "ArgumentList")
|
|
743
|
+
return UNKNOWN;
|
|
744
|
+
const raw = firstTerminalValue(eqname);
|
|
745
|
+
if (!raw)
|
|
746
|
+
return UNKNOWN;
|
|
747
|
+
const { prefix, localName, uri } = parseEQName(raw);
|
|
748
|
+
const qname = { localName, namespaceUri: uri ?? resolvePrefix(prefix, analysis) };
|
|
749
|
+
const args = directChildrenOf(argList, "Argument");
|
|
750
|
+
const piped = t;
|
|
751
|
+
t = inferCallResult(qname, () => [piped, ...args.map((a) => inferExprType(a, varTypes, analysis, allFns))], args.length + 1, allFns);
|
|
752
|
+
i++; // skip the ArgumentList we just consumed
|
|
753
|
+
}
|
|
754
|
+
return t ?? UNKNOWN;
|
|
755
|
+
}
|
|
756
|
+
function flworClauses(flwor) {
|
|
757
|
+
const out = [];
|
|
758
|
+
const unwrap = (n) => {
|
|
759
|
+
if (isTerminal(n))
|
|
760
|
+
return;
|
|
761
|
+
if (n.type === "InitialClause" || n.type === "IntermediateClause") {
|
|
762
|
+
for (const c of n.children)
|
|
763
|
+
unwrap(c);
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
out.push(n);
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
for (const c of flwor.children)
|
|
770
|
+
unwrap(c);
|
|
771
|
+
return out;
|
|
772
|
+
}
|
|
773
|
+
function bindVarName(nameNode, analysis, t, scope) {
|
|
774
|
+
if (!nameNode)
|
|
775
|
+
return null;
|
|
776
|
+
const raw = firstTerminalValue(nameNode);
|
|
777
|
+
if (!raw)
|
|
778
|
+
return null;
|
|
779
|
+
const { prefix, localName, uri } = parseEQName(raw);
|
|
780
|
+
const key = qnameKey({ prefix, localName, namespaceUri: uri ?? (prefix ? resolvePrefix(prefix, analysis) : "") });
|
|
781
|
+
scope.set(key, t);
|
|
782
|
+
return key;
|
|
783
|
+
}
|
|
784
|
+
function processFLWOR(flwor, outer, analysis, allFns, visitor = {}) {
|
|
785
|
+
const scope = new Map(outer);
|
|
786
|
+
const boundHere = [];
|
|
787
|
+
let card = ONE;
|
|
788
|
+
let result = UNKNOWN;
|
|
789
|
+
const evaluate = (expr) => {
|
|
790
|
+
if (!expr)
|
|
791
|
+
return UNKNOWN;
|
|
792
|
+
visitor.expr?.(expr, scope);
|
|
793
|
+
return inferExprType(expr, scope, analysis, allFns);
|
|
794
|
+
};
|
|
795
|
+
const bind = (nameNode, declared, inferred) => {
|
|
796
|
+
const key = bindVarName(nameNode, analysis, declared ?? inferred, scope);
|
|
797
|
+
if (key)
|
|
798
|
+
boundHere.push(key);
|
|
799
|
+
if (!declared && nameNode)
|
|
800
|
+
visitor.binding?.(nameNode, inferred);
|
|
801
|
+
};
|
|
802
|
+
for (const clause of flworClauses(flwor)) {
|
|
803
|
+
switch (clause.type) {
|
|
804
|
+
case "ForClause":
|
|
805
|
+
for (const b of directChildrenOf(clause, "ForBinding")) {
|
|
806
|
+
const seq = evaluate(directChildOf(b, "ExprSingle"));
|
|
807
|
+
const allowingEmpty = b.children.some((c) => isTerminal(c) && c.value === "allowing");
|
|
808
|
+
let item = itemTypeOf(seq);
|
|
809
|
+
let seqCard = cardOf(seq);
|
|
810
|
+
if (allowingEmpty) {
|
|
811
|
+
item = seqCard.min === 1 ? item : withCard(item, { min: 0, max: 1 });
|
|
812
|
+
seqCard = { min: 1, max: seqCard.max === 0 ? 1 : seqCard.max };
|
|
813
|
+
}
|
|
814
|
+
card = multiplyCard(card, seqCard);
|
|
815
|
+
bind(directChildOf(b, "VarName"), declaredTypeOf(b), item);
|
|
816
|
+
const pos = directChildOf(b, "PositionalVar");
|
|
817
|
+
if (pos)
|
|
818
|
+
bindVarName(directChildOf(pos, "VarName"), analysis, INTEGER, scope);
|
|
819
|
+
}
|
|
820
|
+
break;
|
|
821
|
+
case "LetClause":
|
|
822
|
+
for (const b of directChildrenOf(clause, "LetBinding")) {
|
|
823
|
+
const t = evaluate(directChildOf(b, "ExprSingle"));
|
|
824
|
+
bind(directChildOf(b, "VarName"), declaredTypeOf(b), t);
|
|
825
|
+
}
|
|
826
|
+
break;
|
|
827
|
+
case "WindowClause":
|
|
828
|
+
for (const w of clause.children) {
|
|
829
|
+
if (isTerminal(w))
|
|
830
|
+
continue;
|
|
831
|
+
const seq = evaluate(directChildOf(w, "ExprSingle"));
|
|
832
|
+
const item = itemTypeOf(seq);
|
|
833
|
+
bind(directChildOf(w, "VarName"), declaredTypeOf(w), withCard(item, { min: 1, max: 2 }));
|
|
834
|
+
for (const cond of [directChildOf(w, "WindowStartCondition"), directChildOf(w, "WindowEndCondition")]) {
|
|
835
|
+
if (!cond)
|
|
836
|
+
continue;
|
|
837
|
+
const vars = directChildOf(cond, "WindowVars");
|
|
838
|
+
if (vars) {
|
|
839
|
+
bindVarName(directChildOf(vars, "CurrentItem"), analysis, item, scope);
|
|
840
|
+
const pos = directChildOf(vars, "PositionalVar");
|
|
841
|
+
if (pos)
|
|
842
|
+
bindVarName(directChildOf(pos, "VarName"), analysis, INTEGER, scope);
|
|
843
|
+
bindVarName(directChildOf(vars, "PreviousItem"), analysis, withCard(item, { min: 0, max: 1 }), scope);
|
|
844
|
+
bindVarName(directChildOf(vars, "NextItem"), analysis, withCard(item, { min: 0, max: 1 }), scope);
|
|
845
|
+
}
|
|
846
|
+
evaluate(directChildOf(cond, "ExprSingle"));
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
card = multiplyCard(card, { min: 0, max: 2 });
|
|
850
|
+
break;
|
|
851
|
+
case "WhereClause": {
|
|
852
|
+
const cond = directChildOf(clause, "ExprSingle");
|
|
853
|
+
evaluate(cond);
|
|
854
|
+
// Later clauses only see tuples for which the condition held.
|
|
855
|
+
if (cond)
|
|
856
|
+
for (const [k, t] of narrowScope(cond, scope, true, analysis))
|
|
857
|
+
scope.set(k, t);
|
|
858
|
+
card = { min: 0, max: card.max };
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
case "CountClause":
|
|
862
|
+
bindVarName(directChildOf(clause, "VarName"), analysis, INTEGER, scope);
|
|
863
|
+
break;
|
|
864
|
+
case "GroupByClause": {
|
|
865
|
+
const keys = new Set();
|
|
866
|
+
for (const spec of findAll(clause, "GroupingSpec")) {
|
|
867
|
+
const groupVar = directChildOf(spec, "GroupingVariable");
|
|
868
|
+
const nameNode = groupVar ? directChildOf(groupVar, "VarName") : undefined;
|
|
869
|
+
const init = directChildOf(spec, "ExprSingle");
|
|
870
|
+
let key;
|
|
871
|
+
if (init) {
|
|
872
|
+
const t = itemTypeOf(evaluate(init));
|
|
873
|
+
const declared = declaredTypeOf(spec);
|
|
874
|
+
key = bindVarName(nameNode, analysis, declared ?? t, scope);
|
|
875
|
+
if (!declared && nameNode)
|
|
876
|
+
visitor.binding?.(nameNode, t);
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
const existing = nameNode ? asVarName(nameNode, analysis) : null;
|
|
880
|
+
key = existing ? qnameKey(existing) : null;
|
|
881
|
+
if (key && scope.has(key))
|
|
882
|
+
scope.set(key, itemTypeOf(scope.get(key)));
|
|
883
|
+
}
|
|
884
|
+
if (key)
|
|
885
|
+
keys.add(key);
|
|
886
|
+
}
|
|
887
|
+
// Every other variable bound so far now holds the whole group's values.
|
|
888
|
+
for (const key of boundHere) {
|
|
889
|
+
if (keys.has(key))
|
|
890
|
+
continue;
|
|
891
|
+
const t = scope.get(key);
|
|
892
|
+
if (t)
|
|
893
|
+
scope.set(key, withCard(t, { min: cardOf(t).min, max: cardOf(t).max === 0 ? 0 : 2 }));
|
|
894
|
+
}
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
case "ReturnClause":
|
|
898
|
+
result = evaluate(directChildOf(clause, "ExprSingle"));
|
|
899
|
+
break;
|
|
900
|
+
default:
|
|
901
|
+
// order by, and anything we don't model: just evaluate its sub-expressions in scope.
|
|
902
|
+
visitor.expr?.(clause, scope);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (result.kind === "unknown")
|
|
906
|
+
return UNKNOWN;
|
|
907
|
+
return withCard(result, multiplyCard(card, cardOf(result)));
|
|
908
|
+
}
|
|
909
|
+
function processQuantified(node, outer, analysis, allFns, visitor = {}) {
|
|
910
|
+
const scope = new Map(outer);
|
|
911
|
+
let pendingName;
|
|
912
|
+
let pendingDeclared;
|
|
913
|
+
for (const c of node.children) {
|
|
914
|
+
if (c.type === "VarName") {
|
|
915
|
+
pendingName = c;
|
|
916
|
+
pendingDeclared = undefined;
|
|
917
|
+
}
|
|
918
|
+
else if (c.type === "TypeDeclaration") {
|
|
919
|
+
pendingDeclared = sequenceTypeOf(directChildOf(c, "SequenceType"));
|
|
920
|
+
}
|
|
921
|
+
else if (c.type === "ExprSingle") {
|
|
922
|
+
visitor.expr?.(c, scope);
|
|
923
|
+
if (pendingName) {
|
|
924
|
+
const t = inferExprType(c, scope, analysis, allFns);
|
|
925
|
+
bindVarName(pendingName, analysis, pendingDeclared ?? itemTypeOf(t), scope);
|
|
926
|
+
pendingName = undefined;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return BOOLEAN;
|
|
931
|
+
}
|
|
932
|
+
function processTypeswitch(node, outer, analysis, allFns, visitor = {}) {
|
|
933
|
+
const operand = directChildOf(node, "Expr");
|
|
934
|
+
if (operand)
|
|
935
|
+
visitor.expr?.(operand, outer);
|
|
936
|
+
const operandType = operand ? inferExprType(operand, outer, analysis, allFns) : UNKNOWN;
|
|
937
|
+
let result;
|
|
938
|
+
const branch = (varName, varType, expr) => {
|
|
939
|
+
const scope = new Map(outer);
|
|
940
|
+
bindVarName(varName, analysis, varType, scope);
|
|
941
|
+
if (!expr)
|
|
942
|
+
return;
|
|
943
|
+
visitor.expr?.(expr, scope);
|
|
944
|
+
const t = inferExprType(expr, scope, analysis, allFns);
|
|
945
|
+
result = result === undefined ? t : choiceType(result, t);
|
|
946
|
+
};
|
|
947
|
+
for (const clause of directChildrenOf(node, "CaseClause")) {
|
|
948
|
+
branch(directChildOf(clause, "VarName"), sequenceTypeOf(directChildOf(clause, "SequenceTypeUnion")) ?? UNKNOWN, directChildOf(clause, "ExprSingle"));
|
|
949
|
+
}
|
|
950
|
+
branch(directChildOf(node, "VarName"), operandType, directChildOf(node, "ExprSingle"));
|
|
951
|
+
return result ?? UNKNOWN;
|
|
952
|
+
}
|
|
953
|
+
function collectParamTypes(paramList, analysis, scope) {
|
|
954
|
+
const texts = [];
|
|
955
|
+
for (const param of paramList ? directChildrenOf(paramList, "Param") : []) {
|
|
956
|
+
const declared = declaredTypeOf(param);
|
|
957
|
+
bindVarName(directChildOf(param, "EQName"), analysis, declared ?? UNKNOWN, scope);
|
|
958
|
+
texts.push(declared ? formatType(declared) : "item()*");
|
|
959
|
+
}
|
|
960
|
+
return texts;
|
|
961
|
+
}
|
|
962
|
+
function processInlineFunction(node, outer, analysis, allFns, visitor = {}) {
|
|
963
|
+
// Inline functions close over the variables in scope, but have no context item.
|
|
964
|
+
const scope = new Map(outer);
|
|
965
|
+
scope.delete(CONTEXT_ITEM_KEY);
|
|
966
|
+
const params = collectParamTypes(directChildOf(node, "ParamList"), analysis, scope);
|
|
967
|
+
const body = directChildOf(node, "FunctionBody");
|
|
968
|
+
if (body)
|
|
969
|
+
visitor.expr?.(body, scope);
|
|
970
|
+
const declared = sequenceTypeOf(directChildOf(node, "SequenceType"));
|
|
971
|
+
const ret = declared ?? (body ? inferExprType(body, scope, analysis, allFns) : EMPTY);
|
|
972
|
+
return { kind: "function", name: `function(${params.join(", ")}) as ${typeText(ret, "item()*")}`, occurrence: "" };
|
|
973
|
+
}
|
|
974
|
+
// Unwraps the grammar's single-child precedence chain down to the node that does something.
|
|
975
|
+
function coreExpr(node) {
|
|
976
|
+
let n = node;
|
|
977
|
+
while (!isTerminal(n) && n.type !== "VarRef") {
|
|
978
|
+
const ops = nonTerminalChildren(n);
|
|
979
|
+
if (ops.length !== 1)
|
|
980
|
+
break;
|
|
981
|
+
n = ops[0];
|
|
982
|
+
}
|
|
983
|
+
return n;
|
|
984
|
+
}
|
|
985
|
+
function varKeyOf(node, analysis) {
|
|
986
|
+
const core = coreExpr(node);
|
|
987
|
+
const qname = core.type === "VarRef" ? asVarRef(core, analysis) : null;
|
|
988
|
+
return qname ? qnameKey(qname) : null;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Variable types that hold where `cond` is known to be true (`positive`) or false.
|
|
992
|
+
* Understands `$x instance of T`, `exists($x)`, `empty($x)`, a bare `$x` (its effective
|
|
993
|
+
* boolean value is false for ()), `not(…)`, and conjunctions / disjunctions of those.
|
|
994
|
+
*/
|
|
995
|
+
function narrowScope(cond, scope, positive, analysis) {
|
|
996
|
+
const core = coreExpr(cond);
|
|
997
|
+
const ops = isTerminal(core) ? [] : nonTerminalChildren(core);
|
|
998
|
+
const nonEmpty = (key, out) => {
|
|
999
|
+
const t = key ? out.get(key) : undefined;
|
|
1000
|
+
if (!key || !t || t.kind === "unknown" || t.kind === "empty")
|
|
1001
|
+
return out;
|
|
1002
|
+
const c = cardOf(t);
|
|
1003
|
+
if (c.min === 0)
|
|
1004
|
+
out.set(key, withCard(t, { min: 1, max: c.max }));
|
|
1005
|
+
return out;
|
|
1006
|
+
};
|
|
1007
|
+
if ((core.type === "AndExpr" && positive) || (core.type === "OrExpr" && !positive)) {
|
|
1008
|
+
return ops.reduce((s, op) => narrowScope(op, s, positive, analysis), scope);
|
|
1009
|
+
}
|
|
1010
|
+
if (core.type === "VarRef" && positive)
|
|
1011
|
+
return nonEmpty(varKeyOf(core, analysis), new Map(scope));
|
|
1012
|
+
if (core.type === "InstanceofExpr" && positive && ops.length === 2) {
|
|
1013
|
+
const key = varKeyOf(ops[0], analysis);
|
|
1014
|
+
const narrowed = sequenceTypeOf(directChildOf(core, "SequenceType"));
|
|
1015
|
+
const current = key ? scope.get(key) : undefined;
|
|
1016
|
+
if (!key || !narrowed || narrowed.kind === "unknown")
|
|
1017
|
+
return scope;
|
|
1018
|
+
// Only ever make a type more specific: `$int instance of xs:decimal` tells us nothing new.
|
|
1019
|
+
if (current && current.kind !== "unknown" && !isAssignable(narrowed, current))
|
|
1020
|
+
return scope;
|
|
1021
|
+
return new Map(scope).set(key, narrowed);
|
|
1022
|
+
}
|
|
1023
|
+
if (core.type === "FunctionCall") {
|
|
1024
|
+
const call = asFunctionCall(core, analysis);
|
|
1025
|
+
if (!call || call.qname.namespaceUri !== XMLNS_FN || call.args.length !== 1)
|
|
1026
|
+
return scope;
|
|
1027
|
+
const arg = argExpr(call.args[0]);
|
|
1028
|
+
if (!arg)
|
|
1029
|
+
return scope;
|
|
1030
|
+
const name = call.qname.localName;
|
|
1031
|
+
if (name === "not")
|
|
1032
|
+
return narrowScope(arg, scope, !positive, analysis);
|
|
1033
|
+
if ((name === "exists" && positive) || (name === "empty" && !positive) || (name === "boolean" && positive)) {
|
|
1034
|
+
return nonEmpty(varKeyOf(arg, analysis), new Map(scope));
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return scope;
|
|
1038
|
+
}
|
|
1039
|
+
function processIf(node, scope, analysis, allFns, visitor = {}) {
|
|
1040
|
+
const cond = directChildOf(node, "Expr");
|
|
1041
|
+
if (cond)
|
|
1042
|
+
visitor.expr?.(cond, scope);
|
|
1043
|
+
// `then`/`else` are ExprSingle; XQuery 4's braced `if (c) { … }` has one EnclosedExpr.
|
|
1044
|
+
const branches = [...directChildrenOf(node, "ExprSingle"), ...directChildrenOf(node, "EnclosedExpr")];
|
|
1045
|
+
const types = branches.map((branch, i) => {
|
|
1046
|
+
const branchScope = cond ? narrowScope(cond, scope, i === 0, analysis) : scope;
|
|
1047
|
+
visitor.expr?.(branch, branchScope);
|
|
1048
|
+
return inferExprType(branch, branchScope, analysis, allFns);
|
|
1049
|
+
});
|
|
1050
|
+
if (types.length === 0)
|
|
1051
|
+
return UNKNOWN;
|
|
1052
|
+
return types.length === 1 ? choiceType(types[0], EMPTY) : types.reduce(choiceType);
|
|
1053
|
+
}
|
|
1054
|
+
// `copy $c := E modify U return R` — `$c` is a copy of E, so it has E's type.
|
|
1055
|
+
function processTransform(node, outer, analysis, allFns, visitor = {}) {
|
|
1056
|
+
const scope = new Map(outer);
|
|
1057
|
+
const bindingList = directChildOf(node, "XQUF_CopyBindingList");
|
|
1058
|
+
for (const b of bindingList ? directChildrenOf(bindingList, "XQUF_CopyBinding") : []) {
|
|
1059
|
+
const init = directChildOf(b, "ExprSingle");
|
|
1060
|
+
if (init)
|
|
1061
|
+
visitor.expr?.(init, scope);
|
|
1062
|
+
const t = init ? inferExprType(init, scope, analysis, allFns) : UNKNOWN;
|
|
1063
|
+
const nameNode = directChildOf(b, "VarName");
|
|
1064
|
+
bindVarName(nameNode, analysis, t, scope);
|
|
1065
|
+
if (nameNode)
|
|
1066
|
+
visitor.binding?.(nameNode, t);
|
|
1067
|
+
}
|
|
1068
|
+
const [modifyExpr, returnExpr] = directChildrenOf(node, "ExprSingle");
|
|
1069
|
+
if (modifyExpr)
|
|
1070
|
+
visitor.expr?.(modifyExpr, scope);
|
|
1071
|
+
if (!returnExpr)
|
|
1072
|
+
return UNKNOWN;
|
|
1073
|
+
visitor.expr?.(returnExpr, scope);
|
|
1074
|
+
return inferExprType(returnExpr, scope, analysis, allFns);
|
|
1075
|
+
}
|
|
1076
|
+
// ── Expression inference ──────────────────────────────────────────────────────
|
|
1077
|
+
function nonTerminalChildren(nt) {
|
|
1078
|
+
return nt.children.filter((c) => !isTerminal(c));
|
|
1079
|
+
}
|
|
226
1080
|
export function inferExprType(node, varTypes, analysis, allFns) {
|
|
227
1081
|
const lit = literalKind(node);
|
|
228
1082
|
if (lit)
|
|
@@ -230,6 +1084,7 @@ export function inferExprType(node, varTypes, analysis, allFns) {
|
|
|
230
1084
|
if (isTerminal(node))
|
|
231
1085
|
return UNKNOWN;
|
|
232
1086
|
const nt = node;
|
|
1087
|
+
const operands = nonTerminalChildren(nt);
|
|
233
1088
|
switch (node.type) {
|
|
234
1089
|
case "Literal":
|
|
235
1090
|
case "NumericLiteral": {
|
|
@@ -244,51 +1099,171 @@ export function inferExprType(node, varTypes, analysis, allFns) {
|
|
|
244
1099
|
const qname = asVarRef(node, analysis);
|
|
245
1100
|
return qname ? (varTypes.get(qnameKey(qname)) ?? UNKNOWN) : UNKNOWN;
|
|
246
1101
|
}
|
|
1102
|
+
case "ContextItemExpr":
|
|
1103
|
+
return varTypes.get(CONTEXT_ITEM_KEY) ?? UNKNOWN;
|
|
1104
|
+
case "ParenthesizedExpr":
|
|
1105
|
+
if (operands.length === 0)
|
|
1106
|
+
return EMPTY;
|
|
1107
|
+
break;
|
|
1108
|
+
case "Expr":
|
|
1109
|
+
// Comma operator: `(a, b, c)`.
|
|
1110
|
+
if (operands.length > 1) {
|
|
1111
|
+
return operands.map((o) => inferExprType(o, varTypes, analysis, allFns)).reduce(concatType);
|
|
1112
|
+
}
|
|
1113
|
+
break;
|
|
1114
|
+
case "IfExpr":
|
|
1115
|
+
return processIf(nt, varTypes, analysis, allFns);
|
|
1116
|
+
case "XQUF_TransformExpr":
|
|
1117
|
+
return processTransform(nt, varTypes, analysis, allFns);
|
|
1118
|
+
case "SwitchExpr": {
|
|
1119
|
+
const results = [
|
|
1120
|
+
...directChildrenOf(node, "SwitchCaseClause").map((c) => directChildOf(c, "ExprSingle")),
|
|
1121
|
+
...directChildrenOf(node, "ExprSingle"),
|
|
1122
|
+
];
|
|
1123
|
+
if (results.some((r) => !r))
|
|
1124
|
+
return UNKNOWN;
|
|
1125
|
+
return results.map((r) => inferExprType(r, varTypes, analysis, allFns)).reduce(choiceType);
|
|
1126
|
+
}
|
|
1127
|
+
case "TypeswitchExpr":
|
|
1128
|
+
return processTypeswitch(nt, varTypes, analysis, allFns);
|
|
1129
|
+
case "TryCatchExpr": {
|
|
1130
|
+
const bodies = [...findAll(node, "EnclosedTryTargetExpr"), ...directChildrenOf(node, "CatchClause").map((c) => directChildOf(c, "EnclosedExpr"))];
|
|
1131
|
+
if (bodies.some((b) => !b))
|
|
1132
|
+
return UNKNOWN;
|
|
1133
|
+
return bodies.map((b) => inferExprType(b, varTypes, analysis, allFns)).reduce(choiceType);
|
|
1134
|
+
}
|
|
1135
|
+
case "FLWORExpr":
|
|
1136
|
+
return processFLWOR(node, varTypes, analysis, allFns);
|
|
1137
|
+
case "QuantifiedExpr":
|
|
1138
|
+
return processQuantified(nt, varTypes, analysis, allFns);
|
|
1139
|
+
case "InlineFunctionExpr":
|
|
1140
|
+
return processInlineFunction(nt, varTypes, analysis, allFns);
|
|
1141
|
+
case "NamedFunctionRef": {
|
|
1142
|
+
const ref = asNamedFunctionRef(node, analysis);
|
|
1143
|
+
const fn = ref ? resolveFunction(ref.qname, ref.arity, allFns) : undefined;
|
|
1144
|
+
return fn ? functionTypeOf(fn) : { kind: "function", name: "function(*)", occurrence: "" };
|
|
1145
|
+
}
|
|
1146
|
+
case "ComparisonExpr": {
|
|
1147
|
+
if (operands.length < 3)
|
|
1148
|
+
break;
|
|
1149
|
+
// General comparisons (=, <, …) are always a boolean. Value and node comparisons
|
|
1150
|
+
// (eq, is, <<, …) yield () for an empty operand.
|
|
1151
|
+
if (operands[1].type === "GeneralComp")
|
|
1152
|
+
return BOOLEAN;
|
|
1153
|
+
const [a, b] = [operands[0], operands[2]].map((o) => inferExprType(o, varTypes, analysis, allFns));
|
|
1154
|
+
if (a.kind === "unknown" || b.kind === "unknown")
|
|
1155
|
+
return withCard(BOOLEAN, { min: 0, max: 1 });
|
|
1156
|
+
if (a.kind === "empty" || b.kind === "empty")
|
|
1157
|
+
return EMPTY;
|
|
1158
|
+
return cardOf(a).min === 1 && cardOf(b).min === 1 ? BOOLEAN : withCard(BOOLEAN, { min: 0, max: 1 });
|
|
1159
|
+
}
|
|
1160
|
+
case "OrExpr":
|
|
1161
|
+
case "AndExpr":
|
|
1162
|
+
case "InstanceofExpr":
|
|
1163
|
+
case "CastableExpr":
|
|
1164
|
+
if (operands.length > 1)
|
|
1165
|
+
return BOOLEAN;
|
|
1166
|
+
break;
|
|
1167
|
+
case "StringConcatExpr":
|
|
1168
|
+
if (operands.length > 1)
|
|
1169
|
+
return STRING;
|
|
1170
|
+
break;
|
|
1171
|
+
case "StringConstructor":
|
|
1172
|
+
return STRING;
|
|
1173
|
+
case "CastExpr":
|
|
1174
|
+
if (operands.length > 1)
|
|
1175
|
+
return sequenceTypeOf(directChildOf(node, "SingleType")) ?? UNKNOWN;
|
|
1176
|
+
break;
|
|
1177
|
+
case "TreatExpr":
|
|
1178
|
+
if (operands.length > 1)
|
|
1179
|
+
return sequenceTypeOf(directChildOf(node, "SequenceType")) ?? UNKNOWN;
|
|
1180
|
+
break;
|
|
1181
|
+
case "UnaryExpr": {
|
|
1182
|
+
// Every expression passes through UnaryExpr; only a leading +/- makes it arithmetic.
|
|
1183
|
+
const operand = directChildOf(node, "ValueExpr");
|
|
1184
|
+
if (!operand || !nt.children.some(isTerminal))
|
|
1185
|
+
break;
|
|
1186
|
+
// Same operand rules as binary arithmetic: `-$x` behaves like `0 - $x` type-wise.
|
|
1187
|
+
return arithmeticResult("*", inferExprType(operand, varTypes, analysis, allFns), INTEGER);
|
|
1188
|
+
}
|
|
247
1189
|
case "AdditiveExpr":
|
|
248
1190
|
case "MultiplicativeExpr": {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
const t = inferExprType(c, varTypes, analysis, allFns);
|
|
258
|
-
if (result === undefined) {
|
|
259
|
-
result = t;
|
|
260
|
-
}
|
|
261
|
-
else if (pendingOp) {
|
|
262
|
-
result = arithmeticResult(pendingOp, result, t);
|
|
263
|
-
pendingOp = undefined;
|
|
264
|
-
}
|
|
1191
|
+
const arith = asArithmeticExpr(node);
|
|
1192
|
+
if (!arith)
|
|
1193
|
+
break;
|
|
1194
|
+
let result = inferExprType(arith.operands[0], varTypes, analysis, allFns);
|
|
1195
|
+
for (let i = 0; i < arith.operators.length; i++) {
|
|
1196
|
+
const t = inferExprType(arith.operands[i + 1], varTypes, analysis, allFns);
|
|
1197
|
+
result = arithmeticResult(arith.operators[i], result, t);
|
|
265
1198
|
}
|
|
266
|
-
return result
|
|
1199
|
+
return result;
|
|
267
1200
|
}
|
|
1201
|
+
case "IntersectExceptExpr":
|
|
1202
|
+
// `a intersect b` / `a except b` select a subset of `a`.
|
|
1203
|
+
if (operands.length > 1) {
|
|
1204
|
+
const left = inferExprType(operands[0], varTypes, analysis, allFns);
|
|
1205
|
+
return left.kind === "node" ? withCard(left, { min: 0, max: cardOf(left).max }) : NODE_STEP;
|
|
1206
|
+
}
|
|
1207
|
+
break;
|
|
1208
|
+
case "UnionExpr":
|
|
1209
|
+
if (operands.length > 1) {
|
|
1210
|
+
const types = operands.map((o) => inferExprType(o, varTypes, analysis, allFns));
|
|
1211
|
+
const nodes = types.filter((t) => t.kind === "node");
|
|
1212
|
+
if (nodes.length !== types.length)
|
|
1213
|
+
return NODE_STEP;
|
|
1214
|
+
return withCard(nodes.reduce((a, b) => itemUnion(a, b)), { min: 0, max: 2 });
|
|
1215
|
+
}
|
|
1216
|
+
break;
|
|
1217
|
+
case "SimpleMapExpr": {
|
|
1218
|
+
if (operands.length < 2)
|
|
1219
|
+
break;
|
|
1220
|
+
let t = inferExprType(operands[0], varTypes, analysis, allFns);
|
|
1221
|
+
let card = cardOf(t);
|
|
1222
|
+
for (const next of operands.slice(1)) {
|
|
1223
|
+
if (t.kind === "unknown")
|
|
1224
|
+
return UNKNOWN;
|
|
1225
|
+
const scope = new Map(varTypes);
|
|
1226
|
+
scope.set(CONTEXT_ITEM_KEY, itemTypeOf(t));
|
|
1227
|
+
t = inferExprType(next, scope, analysis, allFns);
|
|
1228
|
+
card = multiplyCard(card, cardOf(t));
|
|
1229
|
+
}
|
|
1230
|
+
return withCard(t, card);
|
|
1231
|
+
}
|
|
1232
|
+
case "ArrowExpr":
|
|
1233
|
+
if (operands.length > 1)
|
|
1234
|
+
return inferArrowExpr(nt, varTypes, analysis, allFns);
|
|
1235
|
+
break;
|
|
1236
|
+
case "PostfixExpr":
|
|
1237
|
+
if (operands.length > 1)
|
|
1238
|
+
return inferPostfixExpr(nt, varTypes, analysis, allFns);
|
|
1239
|
+
break;
|
|
1240
|
+
case "MapConstructor":
|
|
1241
|
+
return inferMapConstructor(nt, varTypes, analysis, allFns);
|
|
1242
|
+
case "SquareArrayConstructor":
|
|
1243
|
+
case "CurlyArrayConstructor":
|
|
1244
|
+
return inferArrayConstructor(nt, varTypes, analysis, allFns);
|
|
268
1245
|
case "FunctionCall": {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
// misidentifying it as the function's plain return type.
|
|
272
|
-
const argList = directChildOf(node, "ArgumentList");
|
|
273
|
-
if (argList && findAll(argList, "ArgumentPlaceholder").length > 0)
|
|
1246
|
+
const call = asFunctionCall(node, analysis);
|
|
1247
|
+
if (!call)
|
|
274
1248
|
return UNKNOWN;
|
|
275
|
-
|
|
1249
|
+
// Partial application — one or more `?` placeholders: a function over the missing arguments.
|
|
1250
|
+
const placeholders = call.args.map((a) => directChildOf(a, "ArgumentPlaceholder") !== undefined);
|
|
1251
|
+
if (placeholders.some(Boolean)) {
|
|
1252
|
+
const fn = resolveFunction(call.qname, call.args.length, allFns);
|
|
1253
|
+
if (!fn || fn.variadic)
|
|
1254
|
+
return { kind: "function", name: "function(*)", occurrence: "" };
|
|
1255
|
+
const params = fn.params.filter((_, i) => placeholders[i]).map((p) => p.type ?? "item()*");
|
|
1256
|
+
return { kind: "function", name: `function(${params.join(", ")}) as ${fn.returnType ?? "item()*"}`, occurrence: "" };
|
|
1257
|
+
}
|
|
1258
|
+
return inferCallResult(call.qname, () => call.args.map((a) => inferExprType(a, varTypes, analysis, allFns)), call.args.length, allFns);
|
|
276
1259
|
}
|
|
277
1260
|
case "PathExpr":
|
|
278
|
-
case "RelativePathExpr":
|
|
1261
|
+
case "RelativePathExpr":
|
|
279
1262
|
if (!isPathExpr(node))
|
|
280
1263
|
break;
|
|
281
|
-
|
|
282
|
-
// provably isn't a node (a literal, an atomic function result, etc.), the whole
|
|
283
|
-
// path can't be node-producing either, regardless of which step it is.
|
|
284
|
-
const steps = nt.children.filter((c) => !isTerminal(c));
|
|
285
|
-
const definitelyNotNodes = steps.some((step) => {
|
|
286
|
-
const stepType = inferExprType(step, varTypes, analysis, allFns);
|
|
287
|
-
return stepType.kind !== "unknown" && stepType.kind !== "node" && stepType.kind !== "item";
|
|
288
|
-
});
|
|
289
|
-
return definitelyNotNodes ? UNKNOWN : NODE_STEP;
|
|
290
|
-
}
|
|
1264
|
+
return inferPathExpr(nt, varTypes, analysis, allFns);
|
|
291
1265
|
case "AxisStep":
|
|
1266
|
+
return axisStepType(node);
|
|
292
1267
|
case "ForwardStep":
|
|
293
1268
|
case "ReverseStep":
|
|
294
1269
|
case "AbbrevForwardStep":
|
|
@@ -304,20 +1279,22 @@ export function inferExprType(node, varTypes, analysis, allFns) {
|
|
|
304
1279
|
case "CompTextConstructor":
|
|
305
1280
|
return { kind: "node", name: "text", occurrence: "" };
|
|
306
1281
|
case "CompCommentConstructor":
|
|
1282
|
+
case "DirCommentConstructor":
|
|
307
1283
|
return { kind: "node", name: "comment", occurrence: "" };
|
|
1284
|
+
case "CompPIConstructor":
|
|
1285
|
+
case "DirPIConstructor":
|
|
1286
|
+
return { kind: "node", name: "processing-instruction", occurrence: "" };
|
|
308
1287
|
case "RangeExpr": {
|
|
309
1288
|
// `e1 to e2` — only a genuine range (two operands) produces xs:integer*;
|
|
310
1289
|
// with no "to" present the grammar still wraps a single child, which
|
|
311
1290
|
// falls through to the generic single-child unwrap below.
|
|
312
|
-
|
|
313
|
-
if (kids.length === 2)
|
|
1291
|
+
if (operands.length === 2)
|
|
314
1292
|
return RANGE_SEQUENCE;
|
|
315
1293
|
break;
|
|
316
1294
|
}
|
|
317
1295
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
return inferExprType(ntChildren[0], varTypes, analysis, allFns);
|
|
1296
|
+
if (operands.length === 1)
|
|
1297
|
+
return inferExprType(operands[0], varTypes, analysis, allFns);
|
|
321
1298
|
return UNKNOWN;
|
|
322
1299
|
}
|
|
323
1300
|
// ── Type name formatting ──────────────────────────────────────────────────────
|
|
@@ -330,29 +1307,160 @@ export function formatType(t) {
|
|
|
330
1307
|
return `item()${t.occurrence}`;
|
|
331
1308
|
if (t.kind === "node")
|
|
332
1309
|
return `${t.name ?? "node"}()${t.occurrence}`;
|
|
1310
|
+
// `function(…) as T*` would read as a function returning T*; parenthesize the item type.
|
|
1311
|
+
if (t.kind === "function" && t.occurrence && t.name?.includes(" as "))
|
|
1312
|
+
return `(${t.name})${t.occurrence}`;
|
|
333
1313
|
return `${t.name ?? t.kind}${t.occurrence}`;
|
|
334
1314
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
1315
|
+
function walkScoped(node, scope, analysis, allFns, hooks) {
|
|
1316
|
+
if (isTerminal(node))
|
|
1317
|
+
return;
|
|
1318
|
+
const nt = node;
|
|
1319
|
+
hooks.onNode?.(node, scope);
|
|
1320
|
+
const visitor = {
|
|
1321
|
+
expr: (n, s) => walkScoped(n, s, analysis, allFns, hooks),
|
|
1322
|
+
binding: hooks.onBinding,
|
|
1323
|
+
};
|
|
1324
|
+
switch (node.type) {
|
|
1325
|
+
case "FLWORExpr":
|
|
1326
|
+
processFLWOR(node, scope, analysis, allFns, visitor);
|
|
1327
|
+
return;
|
|
1328
|
+
case "QuantifiedExpr":
|
|
1329
|
+
processQuantified(nt, scope, analysis, allFns, visitor);
|
|
1330
|
+
return;
|
|
1331
|
+
case "TypeswitchExpr":
|
|
1332
|
+
processTypeswitch(nt, scope, analysis, allFns, visitor);
|
|
1333
|
+
return;
|
|
1334
|
+
case "InlineFunctionExpr":
|
|
1335
|
+
processInlineFunction(nt, scope, analysis, allFns, visitor);
|
|
1336
|
+
return;
|
|
1337
|
+
case "IfExpr":
|
|
1338
|
+
processIf(nt, scope, analysis, allFns, visitor);
|
|
1339
|
+
return;
|
|
1340
|
+
case "XQUF_TransformExpr":
|
|
1341
|
+
processTransform(nt, scope, analysis, allFns, visitor);
|
|
1342
|
+
return;
|
|
343
1343
|
}
|
|
344
|
-
|
|
1344
|
+
for (const child of nt.children)
|
|
1345
|
+
walkScoped(child, scope, analysis, allFns, hooks);
|
|
345
1346
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
1347
|
+
/**
|
|
1348
|
+
* Fills in `returnType` for functions in this module declared without `as …`, inferred from
|
|
1349
|
+
* their bodies. Iterates to a fixpoint (bounded) so functions calling other untyped functions,
|
|
1350
|
+
* declared before or after them, resolve too; directly recursive functions stay unknown unless
|
|
1351
|
+
* a non-recursive branch alone determines the type. Callers use the returned list in place of
|
|
1352
|
+
* `allFns` so every call site — hints and type checks alike — sees the inferred types.
|
|
1353
|
+
*/
|
|
1354
|
+
export function withInferredReturnTypes(ast, analysis, allFns, outerScope = new Map()) {
|
|
1355
|
+
const bodies = new Map();
|
|
1356
|
+
for (const annotated of findAll(ast, "AnnotatedDecl")) {
|
|
1357
|
+
const decl = directChildOf(annotated, "FunctionDecl");
|
|
1358
|
+
const body = decl ? directChildOf(decl, "FunctionBody") : undefined;
|
|
1359
|
+
if (decl && body && !directChildOf(decl, "SequenceType"))
|
|
1360
|
+
bodies.set(annotated.start, { decl, body });
|
|
354
1361
|
}
|
|
1362
|
+
const untyped = analysis.functions.filter((f) => !f.returnType && f.sourceOffset !== undefined && bodies.has(f.sourceOffset));
|
|
1363
|
+
if (untyped.length === 0)
|
|
1364
|
+
return allFns;
|
|
1365
|
+
const inferred = new Map();
|
|
1366
|
+
let fns = allFns;
|
|
1367
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
1368
|
+
let changed = false;
|
|
1369
|
+
const moduleTypes = new Map(outerScope);
|
|
1370
|
+
walkModuleVariables(ast, moduleTypes, analysis, fns, {});
|
|
1371
|
+
for (const fn of untyped) {
|
|
1372
|
+
const { decl, body } = bodies.get(fn.sourceOffset);
|
|
1373
|
+
const scope = new Map(moduleTypes);
|
|
1374
|
+
collectParamTypes(directChildOf(decl, "ParamList"), analysis, scope);
|
|
1375
|
+
const t = inferExprType(body, scope, analysis, fns);
|
|
1376
|
+
const text = formatType(t);
|
|
1377
|
+
// Only keep types that round-trip through the signature parser the call sites use.
|
|
1378
|
+
if (t.kind === "unknown" || parseType(text).kind === "unknown" || inferred.get(fn) === text)
|
|
1379
|
+
continue;
|
|
1380
|
+
inferred.set(fn, text);
|
|
1381
|
+
changed = true;
|
|
1382
|
+
}
|
|
1383
|
+
if (!changed)
|
|
1384
|
+
break;
|
|
1385
|
+
fns = allFns.map((f) => (inferred.has(f) ? { ...f, returnType: inferred.get(f) } : f));
|
|
1386
|
+
}
|
|
1387
|
+
return fns;
|
|
355
1388
|
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Types of variables visible in this module without being declared in its source: variables
|
|
1391
|
+
* of imported modules and runtime-predeclared ones. A declared `as` type is used as written;
|
|
1392
|
+
* an untyped imported variable is inferred from its initializer when that module's AST is at
|
|
1393
|
+
* hand (it isn't for lightweight glob summaries).
|
|
1394
|
+
*/
|
|
1395
|
+
export function externalVariableTypes(analysis, imported, allFns) {
|
|
1396
|
+
const out = new Map();
|
|
1397
|
+
const add = (key, t) => {
|
|
1398
|
+
if (t && t.kind !== "unknown")
|
|
1399
|
+
out.set(key, t);
|
|
1400
|
+
};
|
|
1401
|
+
for (const v of analysis.moduleVariables) {
|
|
1402
|
+
if (v.offset < 0 && v.type)
|
|
1403
|
+
add(qnameKey(v.qname), parseType(v.type));
|
|
1404
|
+
}
|
|
1405
|
+
const seen = new Set([analysis]);
|
|
1406
|
+
for (const module of imported) {
|
|
1407
|
+
if (seen.has(module))
|
|
1408
|
+
continue; // the same module is registered under its URI and its `at` path
|
|
1409
|
+
seen.add(module);
|
|
1410
|
+
let inferred;
|
|
1411
|
+
for (const v of module.moduleVariables) {
|
|
1412
|
+
const key = qnameKey(v.qname);
|
|
1413
|
+
if (v.type) {
|
|
1414
|
+
add(key, parseType(v.type));
|
|
1415
|
+
}
|
|
1416
|
+
else if (module.ast) {
|
|
1417
|
+
if (!inferred) {
|
|
1418
|
+
inferred = new Map();
|
|
1419
|
+
walkModuleVariables(module.ast, inferred, module, allFns, {});
|
|
1420
|
+
}
|
|
1421
|
+
add(key, inferred.get(key));
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
return out;
|
|
1426
|
+
}
|
|
1427
|
+
// Module variables in declaration order: declared type, else inferred from the initializer.
|
|
1428
|
+
function walkModuleVariables(ast, moduleTypes, analysis, allFns, hooks) {
|
|
1429
|
+
for (const decl of findAll(ast, "VarDecl")) {
|
|
1430
|
+
const value = directChildOf(decl, "VarValue");
|
|
1431
|
+
if (value && (hooks.onNode || hooks.onBinding))
|
|
1432
|
+
walkScoped(value, moduleTypes, analysis, allFns, hooks);
|
|
1433
|
+
const declared = declaredTypeOf(decl);
|
|
1434
|
+
const inferred = value ? inferExprType(value, moduleTypes, analysis, allFns) : UNKNOWN;
|
|
1435
|
+
const nameNode = directChildOf(decl, "VarName");
|
|
1436
|
+
bindVarName(nameNode, analysis, declared ?? inferred, moduleTypes);
|
|
1437
|
+
if (!declared && value && nameNode)
|
|
1438
|
+
hooks.onBinding?.(nameNode, inferred);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Walks a whole module with lexically correct variable scopes: module variables (declared or
|
|
1443
|
+
* inferred, in declaration order), function parameters inside each function body, and
|
|
1444
|
+
* FLWOR / quantified / typeswitch / inline-function bindings within their own sub-trees.
|
|
1445
|
+
*/
|
|
1446
|
+
export function walkModuleScopes(ast, analysis, allFns, hooks, outerScope = new Map()) {
|
|
1447
|
+
const moduleTypes = new Map(outerScope);
|
|
1448
|
+
walkModuleVariables(ast, moduleTypes, analysis, allFns, hooks);
|
|
1449
|
+
for (const annotated of findAll(ast, "AnnotatedDecl")) {
|
|
1450
|
+
const decl = directChildOf(annotated, "FunctionDecl");
|
|
1451
|
+
const body = decl ? directChildOf(decl, "FunctionBody") : undefined;
|
|
1452
|
+
if (!decl || !body)
|
|
1453
|
+
continue; // external function — nothing to walk
|
|
1454
|
+
const scope = new Map(moduleTypes);
|
|
1455
|
+
collectParamTypes(directChildOf(decl, "ParamList"), analysis, scope);
|
|
1456
|
+
walkScoped(body, scope, analysis, allFns, hooks);
|
|
1457
|
+
}
|
|
1458
|
+
// Top-level query body (present in main modules, absent in library modules).
|
|
1459
|
+
for (const queryBody of findAll(ast, "QueryBody")) {
|
|
1460
|
+
walkScoped(queryBody, new Map(moduleTypes), analysis, allFns, hooks);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
// ── Type checking ─────────────────────────────────────────────────────────────
|
|
356
1464
|
// Check a single FunctionCall node against the known function signatures.
|
|
357
1465
|
function typeCheckCall(callNode, varTypes, analysis, allFns, errors) {
|
|
358
1466
|
const call = asFunctionCall(callNode, analysis);
|
|
@@ -399,55 +1507,40 @@ function typeCheckCall(callNode, varTypes, analysis, allFns, errors) {
|
|
|
399
1507
|
}
|
|
400
1508
|
}
|
|
401
1509
|
}
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (
|
|
1510
|
+
// Flags `a op b` chains where an operand is statically a string or boolean: neither is ever a
|
|
1511
|
+
// valid operand of an arithmetic operator (XPTY0004). Deliberately conservative: operands of
|
|
1512
|
+
// unknown or imprecise type, and empty operands, are not reported.
|
|
1513
|
+
function typeCheckArithmetic(node, varTypes, analysis, allFns, errors) {
|
|
1514
|
+
const arith = asArithmeticExpr(node);
|
|
1515
|
+
if (!arith)
|
|
408
1516
|
return;
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (binding?.typeStr)
|
|
422
|
-
scopeTypes.set(qnameKey(binding.qname), parseType(binding.typeStr));
|
|
423
|
-
}
|
|
424
|
-
if (node.type === "FunctionCall") {
|
|
425
|
-
typeCheckCall(node, scopeTypes, analysis, allFns, errors);
|
|
426
|
-
}
|
|
427
|
-
for (const child of nt.children) {
|
|
428
|
-
walkScope(child, scopeTypes, moduleTypes, text, analysis, allFns, errors);
|
|
1517
|
+
for (const operand of arith.operands) {
|
|
1518
|
+
const t = atomize(inferExprType(operand, varTypes, analysis, allFns));
|
|
1519
|
+
if (t.kind !== "atomic" || !t.name)
|
|
1520
|
+
continue;
|
|
1521
|
+
if (!isAtomicSubtype(t.name, "xs:string") && !isAtomicSubtype(t.name, "xs:boolean"))
|
|
1522
|
+
continue;
|
|
1523
|
+
errors.push({
|
|
1524
|
+
message: `Arithmetic operand of type ${formatType(t)} is not numeric, date/time or duration [XPTY0004]`,
|
|
1525
|
+
code: "XPTY0004",
|
|
1526
|
+
offset: operand.start,
|
|
1527
|
+
length: (operand.end ?? operand.start + 1) - operand.start,
|
|
1528
|
+
});
|
|
429
1529
|
}
|
|
430
1530
|
}
|
|
431
1531
|
// ── Main entry point ─────────────────────────────────────────────────────────
|
|
432
|
-
export function checkTypes(ast,
|
|
1532
|
+
export function checkTypes(ast, _text, analysis, importedAnalyses) {
|
|
433
1533
|
const errors = [];
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
collectParams(directChildOf(decl, "ParamList"), text, analysis, scopeTypes);
|
|
446
|
-
walkScope(body, scopeTypes, moduleTypes, text, analysis, allFns, errors);
|
|
447
|
-
}
|
|
448
|
-
// Top-level query body (present in main modules, absent in library modules).
|
|
449
|
-
for (const queryBody of findAll(ast, "QueryBody")) {
|
|
450
|
-
walkScope(queryBody, new Map(moduleTypes), moduleTypes, text, analysis, allFns, errors);
|
|
451
|
-
}
|
|
1534
|
+
const declaredFns = allFunctionsFlat(analysis, importedAnalyses);
|
|
1535
|
+
const outerScope = externalVariableTypes(analysis, importedAnalyses.values(), declaredFns);
|
|
1536
|
+
const allFns = withInferredReturnTypes(ast, analysis, declaredFns, outerScope);
|
|
1537
|
+
walkModuleScopes(ast, analysis, allFns, {
|
|
1538
|
+
onNode: (node, scope) => {
|
|
1539
|
+
if (node.type === "FunctionCall")
|
|
1540
|
+
typeCheckCall(node, scope, analysis, allFns, errors);
|
|
1541
|
+
else
|
|
1542
|
+
typeCheckArithmetic(node, scope, analysis, allFns, errors);
|
|
1543
|
+
},
|
|
1544
|
+
}, outerScope);
|
|
452
1545
|
return errors;
|
|
453
1546
|
}
|