fhirpath-ts 0.1.0 → 0.2.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 (120) hide show
  1. package/README.md +50 -18
  2. package/dist/analyzer/analyze-dto.d.ts +4 -0
  3. package/dist/analyzer/analyze-dto.d.ts.map +1 -1
  4. package/dist/analyzer/analyze-dto.js +12 -14
  5. package/dist/analyzer/analyze-dto.js.map +1 -1
  6. package/dist/analyzer/analyze.d.ts +22 -6
  7. package/dist/analyzer/analyze.d.ts.map +1 -1
  8. package/dist/analyzer/analyze.js +242 -98
  9. package/dist/analyzer/analyze.js.map +1 -1
  10. package/dist/analyzer/declarations.d.ts +26 -0
  11. package/dist/analyzer/declarations.d.ts.map +1 -0
  12. package/dist/analyzer/declarations.js +68 -0
  13. package/dist/analyzer/declarations.js.map +1 -0
  14. package/dist/analyzer/operator-rules.d.ts +127 -0
  15. package/dist/analyzer/operator-rules.d.ts.map +1 -0
  16. package/dist/analyzer/operator-rules.js +70 -0
  17. package/dist/analyzer/operator-rules.js.map +1 -0
  18. package/dist/analyzer/signatures.d.ts +1097 -10
  19. package/dist/analyzer/signatures.d.ts.map +1 -1
  20. package/dist/analyzer/signatures.js +70 -29
  21. package/dist/analyzer/signatures.js.map +1 -1
  22. package/dist/api/compile.d.ts +32 -8
  23. package/dist/api/compile.d.ts.map +1 -1
  24. package/dist/api/compile.js +2 -0
  25. package/dist/api/compile.js.map +1 -1
  26. package/dist/api/engine.d.ts +42 -16
  27. package/dist/api/engine.d.ts.map +1 -1
  28. package/dist/api/engine.js +2 -1
  29. package/dist/api/engine.js.map +1 -1
  30. package/dist/api/project.d.ts +8 -8
  31. package/dist/api/project.d.ts.map +1 -1
  32. package/dist/api/project.js +31 -19
  33. package/dist/api/project.js.map +1 -1
  34. package/dist/api/strict.d.ts +6 -0
  35. package/dist/api/strict.d.ts.map +1 -0
  36. package/dist/api/strict.js +71 -0
  37. package/dist/api/strict.js.map +1 -0
  38. package/dist/api/tagged.d.ts +4 -4
  39. package/dist/api/tagged.d.ts.map +1 -1
  40. package/dist/api/tagged.js.map +1 -1
  41. package/dist/cli/dto-check.js +7 -3
  42. package/dist/cli/dto-check.js.map +1 -1
  43. package/dist/engine/context.d.ts +3 -1
  44. package/dist/engine/context.d.ts.map +1 -1
  45. package/dist/engine/context.js +5 -1
  46. package/dist/engine/context.js.map +1 -1
  47. package/dist/engine/navigation.d.ts.map +1 -1
  48. package/dist/engine/navigation.js +7 -48
  49. package/dist/engine/navigation.js.map +1 -1
  50. package/dist/engine/type-matching.d.ts.map +1 -1
  51. package/dist/engine/type-matching.js +6 -16
  52. package/dist/engine/type-matching.js.map +1 -1
  53. package/dist/index.d.ts +4 -3
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/parser/parser.d.ts.map +1 -1
  57. package/dist/parser/parser.js +102 -73
  58. package/dist/parser/parser.js.map +1 -1
  59. package/dist/parser/precedence.d.ts +368 -4
  60. package/dist/parser/precedence.d.ts.map +1 -1
  61. package/dist/parser/precedence.js +146 -32
  62. package/dist/parser/precedence.js.map +1 -1
  63. package/dist/r4/generated/type-maps.d.ts +1367 -0
  64. package/dist/r4/generated/type-maps.d.ts.map +1 -1
  65. package/dist/r4/index.d.ts +3 -1
  66. package/dist/r4/index.d.ts.map +1 -1
  67. package/dist/typed/context-maps.d.ts +23 -0
  68. package/dist/typed/context-maps.d.ts.map +1 -0
  69. package/dist/typed/context-maps.js +2 -0
  70. package/dist/typed/context-maps.js.map +1 -0
  71. package/dist/typed/infer.d.ts +99 -229
  72. package/dist/typed/infer.d.ts.map +1 -1
  73. package/dist/typed/infer.js +1 -84
  74. package/dist/typed/infer.js.map +1 -1
  75. package/dist/typed/inference-limits.d.ts +7 -0
  76. package/dist/typed/inference-limits.d.ts.map +1 -0
  77. package/dist/typed/inference-limits.js +6 -0
  78. package/dist/typed/inference-limits.js.map +1 -0
  79. package/dist/typed/metadata-compact.d.ts +189 -0
  80. package/dist/typed/metadata-compact.d.ts.map +1 -0
  81. package/dist/typed/metadata-compact.js +3 -0
  82. package/dist/typed/metadata-compact.js.map +1 -0
  83. package/dist/typed/parser.d.ts +515 -0
  84. package/dist/typed/parser.d.ts.map +1 -0
  85. package/dist/typed/parser.js +2 -0
  86. package/dist/typed/parser.js.map +1 -0
  87. package/dist/values/type-compat.d.ts +8 -0
  88. package/dist/values/type-compat.d.ts.map +1 -1
  89. package/dist/values/type-compat.js +44 -1
  90. package/dist/values/type-compat.js.map +1 -1
  91. package/package.json +20 -1
  92. package/src/analyzer/analyze-dto.ts +21 -17
  93. package/src/analyzer/analyze.ts +335 -109
  94. package/src/analyzer/declarations.ts +120 -0
  95. package/src/analyzer/operator-rules.ts +88 -0
  96. package/src/analyzer/signatures.ts +99 -41
  97. package/src/api/compile.ts +66 -11
  98. package/src/api/engine.ts +97 -33
  99. package/src/api/project.ts +54 -30
  100. package/src/api/strict.ts +112 -0
  101. package/src/api/tagged.ts +5 -5
  102. package/src/cli/dto-check.ts +7 -3
  103. package/src/engine/context.ts +7 -2
  104. package/src/engine/navigation.ts +7 -52
  105. package/src/engine/type-matching.ts +11 -17
  106. package/src/index.ts +25 -2
  107. package/src/parser/parser.ts +106 -74
  108. package/src/parser/precedence.ts +250 -33
  109. package/src/r4/generated/type-maps.ts +1557 -0
  110. package/src/typed/context-maps.ts +41 -0
  111. package/src/typed/infer.ts +157 -425
  112. package/src/typed/inference-limits.ts +9 -0
  113. package/src/typed/metadata-compact.ts +307 -0
  114. package/src/typed/parser.ts +2277 -0
  115. package/src/values/type-compat.ts +48 -1
  116. package/dist/typed/perf-fixture.types.d.ts +0 -136
  117. package/dist/typed/perf-fixture.types.d.ts.map +0 -1
  118. package/dist/typed/perf-fixture.types.js +0 -2
  119. package/dist/typed/perf-fixture.types.js.map +0 -1
  120. package/src/typed/perf-fixture.types.ts +0 -143
@@ -1,13 +1,25 @@
1
1
  import "../functions/install.js";
2
- import { BUILTIN_ENV_VARIABLE_NAMES } from "../engine/context.js";
2
+ import { bareEnvironmentName, BUILTIN_ENV_VARIABLE_NAMES, normalizeEnvKeys } from "../engine/context.js";
3
3
  import { FhirPathSyntaxError } from "../errors.js";
4
4
  import { describeArity, functions } from "../functions/registry.js";
5
5
  import { parse } from "../parser/parser.js";
6
- import { unsatisfiedInput, valueKindOfTypeName } from "../values/type-compat.js";
6
+ import { canonicalFocusType, commonValueKind, resolveByInput, resolveSystemTypeName, rootTypeMatches, SYSTEM_TYPE_LOCAL_NAMES, unsatisfiedInput, } from "../values/type-compat.js";
7
7
  import { FHIR_PRIMITIVE_TO_SYSTEM, typeLocalName } from "../values/typed-value.js";
8
+ import { analyzerEnvironmentVariables, isRuntimeAnalyzerVariable, runtimeAnalyzerEnvironmentVariables, } from "./declarations.js";
9
+ import { applyOperatorResultRule, applyTypeOperatorResultRule } from "./operator-rules.js";
8
10
  import { hasNestedUnboundedQuantifier } from "./regex-safety.js";
9
- import { FUNCTION_SIGNATURES, singleAnd, unionStates, withSingle, } from "./signatures.js";
11
+ import { applyResultRule, FUNCTION_SIGNATURES, singleAnd, unionStates, withSingle, } from "./signatures.js";
10
12
  const UNKNOWN = { types: undefined, single: undefined };
13
+ /** A runtime-analyzed function result no longer has exact facts about its input focus. */
14
+ function callResult(state, runtime) {
15
+ return runtime
16
+ ? {
17
+ types: state.types,
18
+ single: state.single,
19
+ ...(state.targets !== undefined && { targets: state.targets }),
20
+ }
21
+ : state;
22
+ }
11
23
  function emptyScope() {
12
24
  return { vars: new Map(), hasDynamic: false };
13
25
  }
@@ -77,7 +89,11 @@ export function analyzeExpressionDetailed(expression, options) {
77
89
  result: { types: undefined, single: undefined },
78
90
  };
79
91
  }
80
- const analyzer = new Analyzer(options);
92
+ return analyzeAstDetailed(ast, options);
93
+ }
94
+ /** Analyze an already parsed expression, optionally with a runtime-derived input state. */
95
+ function analyzeAstDetailed(ast, options, root) {
96
+ const analyzer = new Analyzer(options, root);
81
97
  const state = analyzer.walk(ast, analyzer.rootState(), emptyScope());
82
98
  return {
83
99
  diagnostics: analyzer.diagnostics,
@@ -85,30 +101,43 @@ export function analyzeExpressionDetailed(expression, options) {
85
101
  result: { types: state.types, single: state.single },
86
102
  };
87
103
  }
104
+ /** Analyze a parsed expression using facts from the actual evaluation focus. */
105
+ export function analyzeRuntimeAstDetailed(ast, options, root) {
106
+ return analyzeAstDetailed(ast, options, root);
107
+ }
88
108
  class Analyzer {
89
109
  diagnostics = [];
90
110
  dependencies = new Set();
91
111
  model;
92
- inputType;
112
+ root;
113
+ runtime;
93
114
  frames = [];
94
115
  /** Every declaration of each host-supplied name; one entry unless the name is overloaded. */
95
116
  customFunctions;
96
117
  declaredVariables;
97
- constructor(options) {
118
+ activeExpressionFunctions = new Set();
119
+ constructor(options, root) {
98
120
  this.model = options?.model;
121
+ this.runtime = root !== undefined;
99
122
  const inputType = options?.inputType;
100
- this.inputType = inputType === undefined ? undefined : (this.model?.resolveType(inputType) ?? inputType);
123
+ this.root =
124
+ root ??
125
+ (inputType === undefined
126
+ ? { types: undefined, single: undefined }
127
+ : { types: [this.model?.resolveType(inputType) ?? inputType], single: true });
101
128
  this.customFunctions = new Map(Object.entries(options?.functions ?? {}).map(([name, declared]) => [
102
129
  name,
103
- ('overloads' in declared ? declared.overloads : [declared]).map(resolvedDeclaration),
104
- ]));
105
- this.declaredVariables = new Map(Object.entries(options?.variables ?? {}).map(([name, variable]) => [
106
- name.startsWith('%') ? name.slice(1) : name,
107
- variable,
130
+ ('overloads' in declared ? declared.overloads : [declared]).map(declaration => resolvedDeclaration(declaration, this.model, this.runtime)),
108
131
  ]));
132
+ this.declaredVariables = new Map(Object.entries(normalizeEnvKeys(options?.variables)));
109
133
  }
110
134
  rootState() {
111
- return this.inputType === undefined ? UNKNOWN : { types: [this.inputType], single: true, rawInput: true };
135
+ return {
136
+ types: this.root.types,
137
+ single: this.root.single,
138
+ rawInput: true,
139
+ ...(this.root.exactTypes !== undefined && { exactTypes: this.root.exactTypes }),
140
+ };
112
141
  }
113
142
  walk(node, input, scope) {
114
143
  switch (node.kind) {
@@ -181,7 +210,7 @@ class Analyzer {
181
210
  }
182
211
  const declared = this.declaredVariables.get(node.name);
183
212
  if (declared !== undefined) {
184
- return { types: declared.types?.map(type => this.canonicalize(type)), single: declared.single };
213
+ return this.declaredVariableState(declared);
185
214
  }
186
215
  switch (node.name) {
187
216
  case 'context':
@@ -228,26 +257,50 @@ class Analyzer {
228
257
  if (input.types !== undefined && input.types.length === 0) {
229
258
  return { types: [], single: true };
230
259
  }
260
+ const exactTypes = input.exactTypes?.filter(type => rootTypeMatches(this.model, type, node.name));
261
+ if (exactTypes !== undefined && exactTypes.length > 0) {
262
+ const types = input.types?.filter(type => rootTypeMatches(this.model, type, node.name));
263
+ return {
264
+ types: types === undefined ? undefined : [...new Set(types)],
265
+ single: input.single,
266
+ exactTypes,
267
+ ...(input.rawInput === true && { rawInput: true }),
268
+ };
269
+ }
231
270
  if (input.types === undefined) {
232
271
  // Even with an unknown input, a root identifier naming a model type anchors
233
272
  // the state — this is what checks `Patient.nope` without an inputType option.
234
- const asType = this.model?.resolveType(node.name);
273
+ const asType = this.model?.resolveType(node.name) ?? resolveSystemTypeName(node.name);
235
274
  if (asType !== undefined) {
236
- return { types: [asType], single: true };
275
+ return {
276
+ types: [asType],
277
+ single: true,
278
+ ...(input.exactTypes !== undefined && {
279
+ exactTypes: input.exactTypes.filter(type => this.model?.isSubtypeOf(type, asType) === true),
280
+ }),
281
+ };
237
282
  }
238
283
  return UNKNOWN;
239
284
  }
240
285
  // Root rule: an identifier naming the (super)type of the context is the context.
241
- if (this.model) {
242
- const asType = this.model.resolveType(node.name);
243
- if (asType !== undefined && input.types.some(type => this.model?.isSubtypeOf(type, asType))) {
286
+ {
287
+ const asType = this.model?.resolveType(node.name) ?? resolveSystemTypeName(node.name);
288
+ let matchingTypes = [];
289
+ if (asType !== undefined) {
290
+ matchingTypes = input.types.filter(type => rootTypeMatches(this.model, type, node.name));
291
+ }
292
+ if (asType !== undefined && matchingTypes.length > 0) {
244
293
  // The runtime matches a type name against the raw input only through the
245
294
  // resourceType discriminator (values/typed-value.ts), so a non-resource
246
295
  // name never matches there and the whole path navigates to empty.
247
- if (input.rawInput === true && !this.isResourceType(asType)) {
296
+ if (input.rawInput === true && !asType.startsWith('System.') && !this.isResourceType(asType)) {
248
297
  this.report('datatype-root', `'${node.name}' is not a resource type, and a type-name root matches only a resource's resourceType, so this always evaluates to empty — navigate from the input with a relative path`, node.span);
249
298
  }
250
- return input;
299
+ return {
300
+ ...input,
301
+ types: matchingTypes,
302
+ ...(input.exactTypes !== undefined && { exactTypes: input.exactTypes }),
303
+ };
251
304
  }
252
305
  }
253
306
  const found = [];
@@ -371,7 +424,8 @@ class Analyzer {
371
424
  const signature = registered !== undefined ? FUNCTION_SIGNATURES[node.name] : this.toSignature(custom?.signature);
372
425
  if (!signature) {
373
426
  this.walkUncheckedArguments(node, input, scope);
374
- return UNKNOWN;
427
+ const expressionResult = this.expressionFunctionResult(node.name, custom, input, scope);
428
+ return expressionResult === undefined ? UNKNOWN : callResult(expressionResult, this.runtime);
375
429
  }
376
430
  this.checkCallInput(node, signature, input);
377
431
  const { argStates, typeTarget } = this.walkArguments(node, signature, input, scope);
@@ -384,7 +438,105 @@ class Analyzer {
384
438
  if ((node.name === 'ofType' || node.name === 'as') && typeTarget !== undefined) {
385
439
  return { types: this.narrowTypes(input, typeTarget, node.span), single: input.single };
386
440
  }
387
- return signature.result(input, argStates);
441
+ const expressionResult = this.expressionFunctionResult(node.name, custom, input, scope);
442
+ if (expressionResult !== undefined) {
443
+ return callResult(expressionResult, this.runtime);
444
+ }
445
+ return callResult(applyResultRule(signature.result, input, argStates), this.runtime);
446
+ }
447
+ /** Apply the expression-body and criteria rules in one place for signed and unsigned declarations. */
448
+ expressionFunctionResult(name, declaration, input, scope) {
449
+ if (declaration?.expression === undefined) {
450
+ for (const possible of declaration?.possibleBodies ?? []) {
451
+ if (possible.expression !== undefined) {
452
+ this.walkExpressionFunction(name, possible.expression, possible.variables, this.expressionBodyInput(possible, input), scope);
453
+ }
454
+ }
455
+ return undefined;
456
+ }
457
+ const expression = declaration.expression;
458
+ const bodyInput = this.expressionBodyInput(declaration, input);
459
+ if (declaration.criteria === true) {
460
+ this.walkExpressionFunction(name, expression, declaration.variables, bodyInput, scope);
461
+ return { types: ['System.Boolean'], single: true };
462
+ }
463
+ if (declaration.signature?.result === undefined) {
464
+ return this.walkExpressionFunction(name, expression, declaration.variables, bodyInput, scope);
465
+ }
466
+ if (this.runtime) {
467
+ this.walkExpressionFunction(name, expression, declaration.variables, bodyInput, scope);
468
+ }
469
+ return undefined;
470
+ }
471
+ /** A declared input type is the contract an expression body must satisfy. */
472
+ expressionBodyInput(declaration, callInput) {
473
+ const declared = declaration.signature?.input?.types;
474
+ if (declared === undefined) {
475
+ return callInput;
476
+ }
477
+ const types = declared
478
+ .map(type => this.model === undefined ? resolveSystemTypeName(typeLocalName(type)) : canonicalFocusType(this.model, type))
479
+ .filter((type) => type !== undefined);
480
+ if (types.length === 0) {
481
+ return callInput;
482
+ }
483
+ return {
484
+ types: [...new Set(types)],
485
+ single: callInput.single,
486
+ ...(callInput.targets !== undefined && { targets: callInput.targets }),
487
+ };
488
+ }
489
+ /** Infer a literal expression-function body under its call focus and temporary local environment declarations. */
490
+ walkExpressionFunction(name, expression, variables, input, callerScope) {
491
+ if (this.activeExpressionFunctions.has(name)) {
492
+ return UNKNOWN;
493
+ }
494
+ let ast = expression.ast;
495
+ if (ast === undefined) {
496
+ try {
497
+ ast = parse(expression.source);
498
+ }
499
+ catch {
500
+ return UNKNOWN;
501
+ }
502
+ }
503
+ const functionScope = forkScope(callerScope);
504
+ for (const [declaredName, variable] of Object.entries(variables ?? {})) {
505
+ const bare = bareEnvironmentName(declaredName);
506
+ // defineVariable() values have priority over environment overlays at runtime.
507
+ if (!functionScope.vars.has(bare)) {
508
+ functionScope.vars.set(bare, this.declaredVariableState(variable));
509
+ }
510
+ }
511
+ const diagnosticCount = this.diagnostics.length;
512
+ this.activeExpressionFunctions.add(name);
513
+ this.frames.push(input);
514
+ try {
515
+ return this.walk(ast, input, functionScope);
516
+ }
517
+ finally {
518
+ if (this.runtime) {
519
+ for (const diagnostic of this.diagnostics.slice(diagnosticCount)) {
520
+ diagnostic.message = `Custom function '${name}': ${diagnostic.message}`;
521
+ }
522
+ }
523
+ else {
524
+ // Body spans index another source string, so callers cannot display these diagnostics correctly.
525
+ this.diagnostics.length = diagnosticCount;
526
+ }
527
+ this.frames.pop();
528
+ this.activeExpressionFunctions.delete(name);
529
+ }
530
+ }
531
+ /** Convert one declared environment value into the same state used for scoped variables. */
532
+ declaredVariableState(declared) {
533
+ const exactTypes = this.runtime && isRuntimeAnalyzerVariable(declared) ? declared.exactTypes : undefined;
534
+ return {
535
+ types: declared.types?.map(type => this.canonicalize(type)),
536
+ single: declared.single,
537
+ ...(declared.targets !== undefined && { targets: declared.targets.map(type => this.canonicalize(type)) }),
538
+ ...(exactTypes !== undefined && { exactTypes }),
539
+ };
388
540
  }
389
541
  /**
390
542
  * Selects a host declaration from the focus. One match keeps its full
@@ -396,9 +548,18 @@ class Analyzer {
396
548
  if (candidates === undefined || candidates.length <= 1) {
397
549
  return candidates?.[0];
398
550
  }
551
+ if (this.runtime && (input.exactTypes !== undefined || this.model === undefined)) {
552
+ const resolution = resolveByInput(this.model, candidates, candidate => candidate.signature?.input?.types, input.exactTypes ?? []);
553
+ return 'resolved' in resolution ? resolution.resolved : mergedDeclaration(candidates);
554
+ }
399
555
  const focus = input.types ?? [];
400
556
  const fitting = candidates.filter(candidate => unsatisfiedInput(this.model, candidate.signature?.input?.types, focus) === undefined);
401
- return fitting.length === 1 ? fitting[0] : mergedDeclaration(fitting.length === 0 ? candidates : fitting);
557
+ if (fitting.length === 1) {
558
+ return fitting[0];
559
+ }
560
+ const possible = fitting.length === 0 ? candidates : fitting;
561
+ const merged = mergedDeclaration(possible);
562
+ return this.runtime ? { ...merged, possibleBodies: possible } : merged;
402
563
  }
403
564
  /** Without a signature the arguments still walk (for their own diagnostics), each in a scope fork. */
404
565
  walkUncheckedArguments(node, input, scope) {
@@ -535,7 +696,11 @@ class Analyzer {
535
696
  return {
536
697
  ...(declared.input !== undefined && { input: declared.input }),
537
698
  ...(declared.args !== undefined && { args: declared.args }),
538
- result: () => ({ types, single }),
699
+ result: {
700
+ kind: 'fixed',
701
+ ...(types !== undefined && { types }),
702
+ ...(single !== undefined && { single }),
703
+ },
539
704
  };
540
705
  }
541
706
  /**
@@ -580,7 +745,7 @@ class Analyzer {
580
745
  /** Check a type name; returns its canonical form, or undefined when it was reported unknown. */
581
746
  checkTypeName(parts, span) {
582
747
  if (parts.length === 2 && parts[0] === 'System') {
583
- if (!SYSTEM_TYPE_NAMES.has(parts[1])) {
748
+ if (!SYSTEM_TYPE_LOCAL_NAMES.has(parts[1])) {
584
749
  this.report('unknown-type', `Unknown type 'System.${parts[1]}'`, span);
585
750
  return undefined;
586
751
  }
@@ -592,11 +757,11 @@ class Analyzer {
592
757
  return undefined;
593
758
  }
594
759
  const resolved = this.model?.resolveType(name);
595
- if (this.model && resolved === undefined && !SYSTEM_TYPE_NAMES.has(name)) {
760
+ if (this.model && resolved === undefined && !SYSTEM_TYPE_LOCAL_NAMES.has(name)) {
596
761
  this.report('unknown-type', `Unknown type '${parts.join('.')}'`, span);
597
762
  return undefined;
598
763
  }
599
- return resolved ?? (SYSTEM_TYPE_NAMES.has(name) ? `System.${name}` : name);
764
+ return resolved ?? (SYSTEM_TYPE_LOCAL_NAMES.has(name) ? `System.${name}` : name);
600
765
  }
601
766
  walkBinary(node, input, scope) {
602
767
  // Operator operands are separate chains: each analyzes against its own scope
@@ -612,23 +777,14 @@ class Analyzer {
612
777
  case 'div':
613
778
  case 'mod': {
614
779
  this.checkArithmetic(node.operator, left, right, node.span);
615
- // Quantity arithmetic yields Quantity (4.0 'g' / 2.0 'm' is 2 'g/m');
616
- // plain division yields Decimal; everything else keeps the operand type.
617
- const quantity = (node.operator === '*' || node.operator === '/') &&
618
- (kindOf(left) === 'Quantity' || kindOf(right) === 'Quantity');
619
- const types = quantity
620
- ? ['System.Quantity']
621
- : node.operator === '/'
622
- ? ['System.Decimal']
623
- : (left.types ?? right.types);
624
- return { types, single: true };
780
+ return applyOperatorResultRule(node.operator, left, right);
625
781
  }
626
782
  case '&':
627
783
  this.requireSingle(left, node.left.span, "'&' expects single-item operands");
628
784
  this.requireSingle(right, node.right.span, "'&' expects single-item operands");
629
785
  this.requireKind(left, 'String', node.left.span, "'&' expects String operands");
630
786
  this.requireKind(right, 'String', node.right.span, "'&' expects String operands");
631
- return { types: ['System.String'], single: true };
787
+ return applyOperatorResultRule(node.operator, left, right);
632
788
  case '<':
633
789
  case '>':
634
790
  case '<=':
@@ -636,13 +792,13 @@ class Analyzer {
636
792
  this.requireSingle(left, node.left.span, `Operator '${node.operator}' expects single-item operands`);
637
793
  this.requireSingle(right, node.right.span, `Operator '${node.operator}' expects single-item operands`);
638
794
  this.checkComparable(left, right, node.span);
639
- return { types: ['System.Boolean'], single: true };
795
+ return applyOperatorResultRule(node.operator, left, right);
640
796
  case '=':
641
797
  case '!=':
642
798
  case '~':
643
799
  case '!~':
644
800
  this.checkEquality(left, right, node.span);
645
- return { types: ['System.Boolean'], single: true };
801
+ return applyOperatorResultRule(node.operator, left, right);
646
802
  case 'and':
647
803
  case 'or':
648
804
  case 'xor':
@@ -651,24 +807,17 @@ class Analyzer {
651
807
  // rule, so only cardinality is checkable here.
652
808
  this.requireSingle(left, node.left.span, `'${node.operator}' expects single-item operands`);
653
809
  this.requireSingle(right, node.right.span, `'${node.operator}' expects single-item operands`);
654
- return { types: ['System.Boolean'], single: true };
810
+ return applyOperatorResultRule(node.operator, left, right);
655
811
  }
656
812
  case '|': {
657
- // A statically empty side contributes nothing: `{} | true` is one item.
658
- if (left.types?.length === 0) {
659
- return right;
660
- }
661
- if (right.types?.length === 0) {
662
- return left;
663
- }
664
- return withSingle(unionStates([left, right]), false);
813
+ return applyOperatorResultRule(node.operator, left, right);
665
814
  }
666
815
  case 'in':
667
816
  case 'contains': {
668
817
  const singletonSide = node.operator === 'in' ? left : right;
669
818
  const singletonSpan = node.operator === 'in' ? node.left.span : node.right.span;
670
819
  this.requireSingle(singletonSide, singletonSpan, `The ${node.operator === 'in' ? 'left' : 'right'} operand of '${node.operator}' must be a single item`);
671
- return { types: ['System.Boolean'], single: true };
820
+ return applyOperatorResultRule(node.operator, left, right);
672
821
  }
673
822
  /* v8 ignore start -- the parser produces no other binary operators */
674
823
  default:
@@ -686,21 +835,16 @@ class Analyzer {
686
835
  const operand = this.walk(node.operand, input, scope);
687
836
  this.requireSingle(operand, node.operand.span, `'${node.operator}' expects a single item operand`);
688
837
  const resolved = this.checkTypeName(node.type.parts, node.type.span);
689
- if (node.operator === 'is') {
690
- return { types: ['System.Boolean'], single: true };
691
- }
692
- if (resolved === undefined) {
693
- return UNKNOWN;
694
- }
695
- return { types: this.narrowTypes(operand, resolved, node.span), single: true };
838
+ const narrowed = node.operator === 'as' && resolved !== undefined ? this.narrowTypes(operand, resolved, node.span) : undefined;
839
+ return applyTypeOperatorResultRule(node.operator, narrowed);
696
840
  }
697
841
  checkArithmetic(operator, left, right, span) {
698
842
  if (isCollection(left) || isCollection(right)) {
699
843
  this.report('singleton-required', `Operator '${operator}' expects single-item operands${NARROW_HINT}`, span);
700
844
  return;
701
845
  }
702
- const leftKind = kindOf(left);
703
- const rightKind = kindOf(right);
846
+ const leftKind = commonValueKind(left.types);
847
+ const rightKind = commonValueKind(right.types);
704
848
  if (leftKind === undefined || rightKind === undefined || leftKind === 'Complex' || rightKind === 'Complex') {
705
849
  return;
706
850
  }
@@ -717,8 +861,8 @@ class Analyzer {
717
861
  }
718
862
  }
719
863
  checkComparable(left, right, span) {
720
- const leftKind = kindOf(left);
721
- const rightKind = kindOf(right);
864
+ const leftKind = commonValueKind(left.types);
865
+ const rightKind = commonValueKind(right.types);
722
866
  if (leftKind === undefined || rightKind === undefined) {
723
867
  return;
724
868
  }
@@ -727,8 +871,8 @@ class Analyzer {
727
871
  }
728
872
  }
729
873
  checkEquality(left, right, span) {
730
- const leftKind = kindOf(left);
731
- const rightKind = kindOf(right);
874
+ const leftKind = commonValueKind(left.types);
875
+ const rightKind = commonValueKind(right.types);
732
876
  if (leftKind === undefined || rightKind === undefined || leftKind === 'Complex' || rightKind === 'Complex') {
733
877
  return;
734
878
  }
@@ -737,7 +881,7 @@ class Analyzer {
737
881
  }
738
882
  }
739
883
  requireKind(state, kind, span, message) {
740
- const actual = kindOf(state);
884
+ const actual = commonValueKind(state.types);
741
885
  if (actual === undefined) {
742
886
  return;
743
887
  }
@@ -752,31 +896,48 @@ class Analyzer {
752
896
  }
753
897
  /** Functions whose first argument is a regular expression pattern. */
754
898
  const REGEX_PATTERN_FUNCTIONS = new Set(['matches', 'matchesFull', 'replaceMatches']);
755
- const SYSTEM_TYPE_NAMES = new Set([
756
- 'Any',
757
- 'Boolean',
758
- 'String',
759
- 'Integer',
760
- 'Long',
761
- 'Decimal',
762
- 'Date',
763
- 'DateTime',
764
- 'Time',
765
- 'Quantity',
766
- ]);
767
899
  /** Appended to singleton-misuse messages so the fix is spelled out, not just the rule. */
768
900
  const NARROW_HINT = ' — narrow it to one item with first(), last(), or single()';
769
901
  /** Statically known to possibly hold more than one item (types known, not a singleton). */
770
902
  function isCollection(state) {
771
903
  return state.types !== undefined && state.single === false;
772
904
  }
773
- function resolvedDeclaration(declared) {
905
+ function resolvedDeclaration(declared, model, runtime) {
774
906
  if (declared.expression === undefined) {
775
- return declared;
907
+ return {
908
+ ...(declared.minArity !== undefined && { minArity: declared.minArity }),
909
+ ...(declared.maxArity !== undefined && { maxArity: declared.maxArity }),
910
+ ...(declared.signature !== undefined && { signature: declared.signature }),
911
+ };
776
912
  }
777
913
  // An expression-defined function takes no arguments, which is how the runtime
778
914
  // calls it (`evaluateHostFunction`).
779
- return { minArity: 0, maxArity: 0, ...(declared.signature !== undefined && { signature: declared.signature }) };
915
+ const expression = resolvedExpression(declared.expression);
916
+ return {
917
+ minArity: 0,
918
+ maxArity: 0,
919
+ ...(declared.signature !== undefined && { signature: declared.signature }),
920
+ ...(expression !== undefined && { expression }),
921
+ ...(declared.criteria !== undefined && { criteria: declared.criteria }),
922
+ ...((declared.env !== undefined || declared.envTypes !== undefined) && {
923
+ variables: runtime
924
+ ? runtimeAnalyzerEnvironmentVariables(declared.env, declared.envTypes, model)
925
+ : analyzerEnvironmentVariables(declared.env, declared.envTypes, model),
926
+ }),
927
+ };
928
+ }
929
+ function resolvedExpression(expression) {
930
+ if (typeof expression === 'string') {
931
+ return { source: expression };
932
+ }
933
+ const compiled = expression;
934
+ if (typeof compiled?.source !== 'string') {
935
+ return undefined;
936
+ }
937
+ return {
938
+ source: compiled.source,
939
+ ...(compiled.ast !== undefined && { ast: compiled.ast }),
940
+ };
780
941
  }
781
942
  /**
782
943
  * What several declarations of one name say together, for a call none of them
@@ -809,7 +970,7 @@ function mergedInput(candidates) {
809
970
  /** The union of the declarations' results, unknown as soon as one of them is. */
810
971
  function mergedResult(candidates) {
811
972
  const union = unionStates(candidates.map(candidate => ({
812
- types: candidate.signature?.result?.types,
973
+ types: candidate.signature?.result?.types === undefined ? undefined : [...candidate.signature.result.types],
813
974
  single: candidate.signature?.result?.single,
814
975
  })));
815
976
  if (union.types === undefined && union.single === undefined) {
@@ -820,23 +981,6 @@ function mergedResult(candidates) {
820
981
  ...(union.single !== undefined && { single: union.single }),
821
982
  };
822
983
  }
823
- /** The behavior kind shared by every candidate type, or undefined when mixed/unknown. */
824
- function kindOf(state) {
825
- if (state.types === undefined || state.types.length === 0) {
826
- return undefined;
827
- }
828
- let kind;
829
- for (const type of state.types) {
830
- const typeKind = valueKindOfTypeName(type);
831
- if (kind === undefined) {
832
- kind = typeKind;
833
- }
834
- else if (kind !== typeKind) {
835
- return undefined;
836
- }
837
- }
838
- return kind;
839
- }
840
984
  function typeSpecifierParts(node) {
841
985
  if (node.kind === 'identifier') {
842
986
  return [node.name];