fhirpath-ts 0.0.0-bootstrap.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
@@ -0,0 +1,120 @@
1
+ import { normalizeEnvKeys } from '../engine/context.ts'
2
+ import type { ModelProvider } from '../model/provider.ts'
3
+ import type { FhirpathTypeDeclaration, FhirpathTypeDeclarations } from '../typed/infer.ts'
4
+ import { OBJECT_TYPE, toCollection, type TypedValue, typeLocalName } from '../values/typed-value.ts'
5
+
6
+ /** A host variable in the analyzer's canonical collection form. */
7
+ export interface AnalyzerVariable {
8
+ types?: string[]
9
+ single?: boolean
10
+ targets?: string[]
11
+ }
12
+
13
+ /** Internal variable state with the exact focus types used by runtime host-function dispatch. */
14
+ export interface RuntimeAnalyzerVariable extends AnalyzerVariable {
15
+ exactTypes: string[]
16
+ }
17
+
18
+ export type AnalyzerVariableState = AnalyzerVariable | RuntimeAnalyzerVariable
19
+
20
+ export function isRuntimeAnalyzerVariable(variable: AnalyzerVariableState): variable is RuntimeAnalyzerVariable {
21
+ return 'exactTypes' in variable
22
+ }
23
+
24
+ /** Convert one public type declaration into analyzer state. */
25
+ export function analyzerVariable(declaration: FhirpathTypeDeclaration): AnalyzerVariable {
26
+ return {
27
+ types: typeof declaration.type === 'string' ? [declaration.type] : [...declaration.type],
28
+ single: declaration.collection === true ? false : true,
29
+ ...(declaration.targets !== undefined && {
30
+ targets: typeof declaration.targets === 'string' ? [declaration.targets] : [...declaration.targets],
31
+ }),
32
+ }
33
+ }
34
+
35
+ /** Convert one runtime collection while retaining the exact types used for host-function dispatch. */
36
+ export function runtimeAnalyzerVariable(
37
+ collection: readonly TypedValue[],
38
+ model: ModelProvider | undefined,
39
+ declaration?: FhirpathTypeDeclaration
40
+ ): RuntimeAnalyzerVariable {
41
+ const inferred = analyzerVariableFromCollection(collection, model)
42
+ return {
43
+ ...(declaration === undefined ? inferred : analyzerVariable(declaration)),
44
+ exactTypes: collection.map(item => item.type),
45
+ }
46
+ }
47
+
48
+ function analyzerVariableFromCollection(
49
+ collection: readonly TypedValue[],
50
+ model: ModelProvider | undefined
51
+ ): AnalyzerVariable {
52
+ const inferred = collection.map(item => inferAnalyzerType(item.type, model))
53
+ const types = inferred.every(type => type !== undefined) ? [...new Set(inferred)] : undefined
54
+ return {
55
+ ...(types !== undefined && types.length > 0 && { types }),
56
+ single: collection.length <= 1,
57
+ }
58
+ }
59
+
60
+ function analyzerVariableFromValue(value: unknown, model: ModelProvider | undefined): AnalyzerVariable {
61
+ return analyzerVariableFromCollection(toCollection(value), model)
62
+ }
63
+
64
+ function inferAnalyzerType(type: string, model: ModelProvider | undefined): string | undefined {
65
+ if (type === OBJECT_TYPE) {
66
+ return undefined
67
+ }
68
+ return type.startsWith('System.') ? type : model?.resolveType(typeLocalName(type))
69
+ }
70
+
71
+ /** Declare every runtime value name and add explicit type information. */
72
+ export function analyzerVariables(
73
+ values: object | undefined,
74
+ declarations: FhirpathTypeDeclarations | undefined
75
+ ): Record<string, AnalyzerVariable> {
76
+ return collectAnalyzerVariables(values, declarations, (_value, declaration) =>
77
+ declaration === undefined ? {} : analyzerVariable(declaration)
78
+ )
79
+ }
80
+
81
+ /** Infer safe types from environment values, with explicit declarations taking precedence. */
82
+ export function analyzerEnvironmentVariables(
83
+ values: object | undefined,
84
+ declarations: FhirpathTypeDeclarations | undefined,
85
+ model: ModelProvider | undefined
86
+ ): Record<string, AnalyzerVariable> {
87
+ return collectAnalyzerVariables(values, declarations, (value, declaration) =>
88
+ declaration === undefined ? analyzerVariableFromValue(value, model) : analyzerVariable(declaration)
89
+ )
90
+ }
91
+
92
+ /** Runtime environment state, retaining actual dispatch types independently of declarations. */
93
+ export function runtimeAnalyzerEnvironmentVariables(
94
+ values: object | undefined,
95
+ declarations: FhirpathTypeDeclarations | undefined,
96
+ model: ModelProvider | undefined
97
+ ): Record<string, AnalyzerVariableState> {
98
+ return collectAnalyzerVariables(values, declarations, (value, declaration) =>
99
+ runtimeAnalyzerVariable(toCollection(value), model, declaration)
100
+ )
101
+ }
102
+
103
+ function collectAnalyzerVariables(
104
+ values: object | undefined,
105
+ declarations: FhirpathTypeDeclarations | undefined,
106
+ inferValue: (value: unknown, declaration: FhirpathTypeDeclaration | undefined) => AnalyzerVariable
107
+ ): Record<string, AnalyzerVariable> {
108
+ const normalizedValues = normalizeEnvKeys(values as Readonly<Record<string, unknown>> | undefined)
109
+ const normalizedDeclarations = normalizeEnvKeys(declarations)
110
+ const variables: Record<string, AnalyzerVariable> = {}
111
+ for (const name of new Set([...Object.keys(normalizedValues), ...Object.keys(normalizedDeclarations)])) {
112
+ const declaration = normalizedDeclarations[name]
113
+ if (Object.hasOwn(normalizedValues, name)) {
114
+ variables[name] = inferValue(normalizedValues[name], declaration)
115
+ } else if (declaration !== undefined) {
116
+ variables[name] = analyzerVariable(declaration)
117
+ }
118
+ }
119
+ return variables
120
+ }
@@ -0,0 +1,88 @@
1
+ import type { BinaryOperator } from '../parser/ast.ts'
2
+ import { commonValueKind } from '../values/type-compat.ts'
3
+ import { type StaticStateLike, unionStates, withSingle } from './signatures.ts'
4
+
5
+ export type OperatorResultRule =
6
+ | { kind: 'fixed'; types: readonly string[]; single: boolean }
7
+ | { kind: 'arithmetic' }
8
+ | { kind: 'union' }
9
+ | { kind: 'narrow' }
10
+
11
+ const BOOLEAN = { kind: 'fixed', types: ['System.Boolean'], single: true } as const
12
+
13
+ /** Result semantics for every runtime binary operator. */
14
+ export const OPERATOR_RESULT_RULES = {
15
+ '*': { kind: 'arithmetic' },
16
+ '/': { kind: 'arithmetic' },
17
+ div: { kind: 'arithmetic' },
18
+ mod: { kind: 'arithmetic' },
19
+ '+': { kind: 'arithmetic' },
20
+ '-': { kind: 'arithmetic' },
21
+ '&': { kind: 'fixed', types: ['System.String'], single: true },
22
+ '|': { kind: 'union' },
23
+ '<': BOOLEAN,
24
+ '>': BOOLEAN,
25
+ '<=': BOOLEAN,
26
+ '>=': BOOLEAN,
27
+ '=': BOOLEAN,
28
+ '~': BOOLEAN,
29
+ '!=': BOOLEAN,
30
+ '!~': BOOLEAN,
31
+ in: BOOLEAN,
32
+ contains: BOOLEAN,
33
+ and: BOOLEAN,
34
+ or: BOOLEAN,
35
+ xor: BOOLEAN,
36
+ implies: BOOLEAN,
37
+ } as const satisfies Record<BinaryOperator, OperatorResultRule>
38
+
39
+ export const TYPE_OPERATOR_RESULT_RULES = {
40
+ is: BOOLEAN,
41
+ as: { kind: 'narrow' },
42
+ } as const satisfies Record<'is' | 'as', OperatorResultRule>
43
+
44
+ /** Interpret a binary rule after the analyzer has reported operand diagnostics. */
45
+ export function applyOperatorResultRule(
46
+ operator: BinaryOperator,
47
+ left: StaticStateLike,
48
+ right: StaticStateLike
49
+ ): StaticStateLike {
50
+ const rule = OPERATOR_RESULT_RULES[operator]
51
+ switch (rule.kind) {
52
+ case 'fixed':
53
+ return { types: [...rule.types], single: rule.single }
54
+ case 'arithmetic': {
55
+ const leftKind = commonValueKind(left.types)
56
+ const rightKind = commonValueKind(right.types)
57
+ const quantity = (operator === '*' || operator === '/') && (leftKind === 'Quantity' || rightKind === 'Quantity')
58
+ return {
59
+ types: quantity ? ['System.Quantity'] : operator === '/' ? ['System.Decimal'] : (left.types ?? right.types),
60
+ single: true,
61
+ }
62
+ }
63
+ case 'union':
64
+ if (left.types?.length === 0) {
65
+ return right
66
+ }
67
+ if (right.types?.length === 0) {
68
+ return left
69
+ }
70
+ return withSingle(unionStates([left, right]), false)
71
+ }
72
+ }
73
+
74
+ /** Interpret a type-operator rule after the analyzer has validated its operand and target. */
75
+ export function applyTypeOperatorResultRule(
76
+ operator: keyof typeof TYPE_OPERATOR_RESULT_RULES,
77
+ narrowedTypes: readonly string[] | undefined
78
+ ): StaticStateLike {
79
+ const rule = TYPE_OPERATOR_RESULT_RULES[operator]
80
+ switch (rule.kind) {
81
+ case 'fixed':
82
+ return { types: [...rule.types], single: rule.single }
83
+ case 'narrow':
84
+ return narrowedTypes === undefined
85
+ ? { types: undefined, single: undefined }
86
+ : { types: [...narrowedTypes], single: true }
87
+ }
88
+ }
@@ -1,6 +1,6 @@
1
1
  import type { ValueKind } from '../values/type-compat.ts'
2
2
 
3
- interface StaticStateLike {
3
+ export interface StaticStateLike {
4
4
  types: string[] | undefined
5
5
  /** True: at most one item. False: may hold several. Undefined: cardinality unknown. */
6
6
  single: boolean | undefined
@@ -35,7 +35,7 @@ export interface InputSpec {
35
35
  kind?: ValueKind
36
36
  singleton?: boolean
37
37
  /** Canonical or local model type names ('CodeableConcept', 'System.String'). */
38
- types?: string[]
38
+ types?: readonly string[]
39
39
  }
40
40
 
41
41
  /**
@@ -46,26 +46,74 @@ export interface InputSpec {
46
46
  */
47
47
  export interface CustomFunctionSignature {
48
48
  input?: InputSpec
49
- args?: ValueArgSpec[]
50
- result?: { types?: string[]; single?: boolean }
49
+ args?: readonly ValueArgSpec[]
50
+ result?: { types?: readonly string[]; single?: boolean }
51
51
  }
52
52
 
53
+ /**
54
+ * Declarative result rules shared by analyzer signatures and the generated
55
+ * type-level rule table. Keeping these as data prevents the two inference
56
+ * implementations from acquiring separate handwritten function semantics.
57
+ */
58
+ export type ResultRule =
59
+ | { kind: 'fixed'; types?: readonly string[]; single?: boolean }
60
+ | { kind: 'input' }
61
+ | { kind: 'input-item' }
62
+ | { kind: 'argument'; index: number }
63
+ | { kind: 'union'; sources: readonly ('input' | number)[]; single: boolean | 'all' }
64
+ | { kind: 'arguments-union' }
65
+ | { kind: 'reference-targets' }
66
+ | { kind: 'unknown' }
67
+
53
68
  export interface FunctionSignature {
54
69
  input?: InputSpec
55
- args?: ArgSpec[]
56
- /**
57
- * Result state from the input state and the analyzed argument states
58
- * (undefined for `type-name` positions and missing optional arguments).
59
- */
60
- result: (input: StaticStateLike, args: readonly (StaticStateLike | undefined)[]) => StaticStateLike
70
+ args?: readonly ArgSpec[]
71
+ result: ResultRule
61
72
  }
62
73
 
63
- const BOOLEAN = (): StaticStateLike => ({ types: ['System.Boolean'], single: true })
64
- const INTEGER = (): StaticStateLike => ({ types: ['System.Integer'], single: true })
65
- const STRING = (): StaticStateLike => ({ types: ['System.String'], single: true })
66
- const DECIMAL = (): StaticStateLike => ({ types: ['System.Decimal'], single: true })
67
- const UNKNOWN = (): StaticStateLike => ({ types: undefined, single: undefined })
68
- const SAME = (input: StaticStateLike): StaticStateLike => input
74
+ const BOOLEAN = { kind: 'fixed', types: ['System.Boolean'], single: true } as const satisfies ResultRule
75
+ const INTEGER = { kind: 'fixed', types: ['System.Integer'], single: true } as const satisfies ResultRule
76
+ const STRING = { kind: 'fixed', types: ['System.String'], single: true } as const satisfies ResultRule
77
+ const DECIMAL = { kind: 'fixed', types: ['System.Decimal'], single: true } as const satisfies ResultRule
78
+ const LONG = { kind: 'fixed', types: ['System.Long'], single: true } as const satisfies ResultRule
79
+ const DATE = { kind: 'fixed', types: ['System.Date'], single: true } as const satisfies ResultRule
80
+ const DATETIME = { kind: 'fixed', types: ['System.DateTime'], single: true } as const satisfies ResultRule
81
+ const TIME = { kind: 'fixed', types: ['System.Time'], single: true } as const satisfies ResultRule
82
+ const QUANTITY = { kind: 'fixed', types: ['System.Quantity'], single: true } as const satisfies ResultRule
83
+ const UNKNOWN = { kind: 'unknown' } as const satisfies ResultRule
84
+ const SAME = { kind: 'input' } as const satisfies ResultRule
85
+ const ITEM = { kind: 'input-item' } as const satisfies ResultRule
86
+
87
+ /** Interpret one declarative result rule for the runtime analyzer. */
88
+ export function applyResultRule(
89
+ rule: ResultRule,
90
+ input: StaticStateLike,
91
+ args: readonly (StaticStateLike | undefined)[]
92
+ ): StaticStateLike {
93
+ switch (rule.kind) {
94
+ case 'fixed':
95
+ return { types: rule.types === undefined ? undefined : [...rule.types], single: rule.single }
96
+ case 'input':
97
+ return input
98
+ case 'input-item':
99
+ return withSingle(input, true)
100
+ case 'argument': {
101
+ const argument = args[rule.index] ?? { types: undefined, single: undefined }
102
+ return withSingle(argument, singleAnd(input.single, argument.single))
103
+ }
104
+ case 'union': {
105
+ const states = rule.sources.map(source => (source === 'input' ? input : args[source]))
106
+ const merged = unionStates(states)
107
+ return rule.single === 'all' ? merged : withSingle(merged, rule.single)
108
+ }
109
+ case 'arguments-union':
110
+ return unionStates([...args])
111
+ case 'reference-targets':
112
+ return { types: input.targets, single: input.single }
113
+ case 'unknown':
114
+ return { types: undefined, single: undefined }
115
+ }
116
+ }
69
117
 
70
118
  /**
71
119
  * The input's candidate types and reference targets at a different cardinality
@@ -78,8 +126,6 @@ export function withSingle(input: StaticStateLike, single: boolean | undefined):
78
126
  : { types: input.types, single, targets: input.targets }
79
127
  }
80
128
 
81
- const ITEM = (input: StaticStateLike): StaticStateLike => withSingle(input, true)
82
-
83
129
  /**
84
130
  * The union of several alternative states (iif branches, coalesce arguments,
85
131
  * merged collections): all candidate types, single only when every alternative
@@ -101,14 +147,21 @@ export function unionStates(states: (StaticStateLike | undefined)[]): StaticStat
101
147
  return targets === undefined ? { types, single } : { types, single, targets }
102
148
  }
103
149
 
104
- const STRING_FN: FunctionSignature = { input: { kind: 'String', singleton: true }, args: ['String'], result: STRING }
105
- const MATH_FN: FunctionSignature = { input: { kind: 'Numeric', singleton: true }, result: DECIMAL }
150
+ const STRING_FN = {
151
+ input: { kind: 'String', singleton: true },
152
+ args: ['String'],
153
+ result: STRING,
154
+ } as const satisfies FunctionSignature
155
+ const MATH_FN = {
156
+ input: { kind: 'Numeric', singleton: true },
157
+ result: DECIMAL,
158
+ } as const satisfies FunctionSignature
106
159
 
107
160
  /**
108
161
  * What the analyzer knows about each function. Functions missing here still get
109
162
  * arity checks from the runtime registry; their results become unknown.
110
163
  */
111
- export const FUNCTION_SIGNATURES: Readonly<Record<string, FunctionSignature>> = {
164
+ const FUNCTION_SIGNATURE_DEFINITIONS = {
112
165
  empty: { result: BOOLEAN },
113
166
  exists: { args: ['expression'], result: BOOLEAN },
114
167
  all: { args: ['expression'], result: BOOLEAN },
@@ -128,7 +181,7 @@ export const FUNCTION_SIGNATURES: Readonly<Record<string, FunctionSignature>> =
128
181
  args: ['expression'],
129
182
  // The projection's analyzed state, collection-ized: single only when both
130
183
  // the input and the projection body are single.
131
- result: (input, args) => withSingle(args[0] ?? UNKNOWN(), singleAnd(input.single, args[0]?.single)),
184
+ result: { kind: 'argument', index: 0 },
132
185
  },
133
186
  repeat: { args: ['expression'], result: UNKNOWN },
134
187
  // ofType/as results narrow to the named type; the analyzer computes that with
@@ -144,12 +197,12 @@ export const FUNCTION_SIGNATURES: Readonly<Record<string, FunctionSignature>> =
144
197
  take: { args: ['Numeric'], result: SAME },
145
198
  intersect: { args: ['any'], result: SAME },
146
199
  exclude: { args: ['any'], result: SAME },
147
- union: { args: ['any'], result: (input, args) => withSingle(unionStates([input, args[0]]), false) },
148
- combine: { args: ['any'], result: (input, args) => withSingle(unionStates([input, args[0]]), false) },
200
+ union: { args: ['any'], result: { kind: 'union', sources: ['input', 0], single: false } },
201
+ combine: { args: ['any'], result: { kind: 'union', sources: ['input', 0], single: false } },
149
202
  iif: {
150
203
  args: ['condition', 'expression', 'expression'],
151
204
  // The union of the branch states; a missing else-branch contributes empty.
152
- result: (_input, args) => unionStates([args[1], args[2] ?? { types: [], single: true }]),
205
+ result: { kind: 'union', sources: [1, 2], single: 'all' },
153
206
  },
154
207
  // not() takes anything a Boolean test accepts (0/1, single items), so no kind pin.
155
208
  not: { input: { singleton: true }, result: BOOLEAN },
@@ -158,7 +211,7 @@ export const FUNCTION_SIGNATURES: Readonly<Record<string, FunctionSignature>> =
158
211
  descendants: { result: UNKNOWN },
159
212
  // A reference resolves to its declared target types (Reference.targetProfile,
160
213
  // HAPI's TypeDetails.targets); an unconstrained reference stays unknown.
161
- resolve: { result: input => ({ types: input.targets, single: input.single }) },
214
+ resolve: { result: { kind: 'reference-targets' } },
162
215
  extension: { args: ['String'], result: UNKNOWN },
163
216
  hasValue: { input: { singleton: true }, result: BOOLEAN },
164
217
  getValue: { input: { singleton: true }, result: UNKNOWN },
@@ -181,13 +234,13 @@ export const FUNCTION_SIGNATURES: Readonly<Record<string, FunctionSignature>> =
181
234
  replaceMatches: { input: { kind: 'String', singleton: true }, args: ['String', 'String'], result: STRING },
182
235
  toChars: {
183
236
  input: { kind: 'String', singleton: true },
184
- result: () => ({ types: ['System.String'], single: false }),
237
+ result: { kind: 'fixed', types: ['System.String'], single: false },
185
238
  },
186
239
  trim: { input: { kind: 'String', singleton: true }, result: STRING },
187
240
  split: {
188
241
  input: { kind: 'String', singleton: true },
189
242
  args: ['String'],
190
- result: () => ({ types: ['System.String'], single: false }),
243
+ result: { kind: 'fixed', types: ['System.String'], single: false },
191
244
  },
192
245
  join: { input: { kind: 'String' }, args: ['String'], result: STRING },
193
246
  encode: STRING_FN,
@@ -205,7 +258,9 @@ export const FUNCTION_SIGNATURES: Readonly<Record<string, FunctionSignature>> =
205
258
  sqrt: MATH_FN,
206
259
  log: { input: { kind: 'Numeric', singleton: true }, args: ['Numeric'], result: DECIMAL },
207
260
  power: { input: { kind: 'Numeric', singleton: true }, args: ['Numeric'], result: UNKNOWN },
208
- aggregate: { args: ['expression', 'any'], result: UNKNOWN },
261
+ // Each iteration replaces the accumulator with the aggregator result. An
262
+ // empty input returns init, when supplied, so both arguments can contribute.
263
+ aggregate: { args: ['expression', 'any'], result: { kind: 'union', sources: [0, 1], single: 'all' } },
209
264
  sum: { input: { kind: 'Numeric' }, result: UNKNOWN },
210
265
  min: { input: { kind: 'Numeric' }, result: UNKNOWN },
211
266
  max: { input: { kind: 'Numeric' }, result: UNKNOWN },
@@ -214,16 +269,16 @@ export const FUNCTION_SIGNATURES: Readonly<Record<string, FunctionSignature>> =
214
269
 
215
270
  toBoolean: { input: { singleton: true }, result: BOOLEAN },
216
271
  toInteger: { input: { singleton: true }, result: INTEGER },
217
- toLong: { input: { singleton: true }, result: () => ({ types: ['System.Long'], single: true }) },
272
+ toLong: { input: { singleton: true }, result: LONG },
218
273
  toDecimal: { input: { singleton: true }, result: DECIMAL },
219
274
  toString: { input: { singleton: true }, result: STRING },
220
- toDate: { input: { singleton: true }, result: () => ({ types: ['System.Date'], single: true }) },
221
- toDateTime: { input: { singleton: true }, result: () => ({ types: ['System.DateTime'], single: true }) },
222
- toTime: { input: { singleton: true }, result: () => ({ types: ['System.Time'], single: true }) },
275
+ toDate: { input: { singleton: true }, result: DATE },
276
+ toDateTime: { input: { singleton: true }, result: DATETIME },
277
+ toTime: { input: { singleton: true }, result: TIME },
223
278
  toQuantity: {
224
279
  input: { singleton: true },
225
280
  args: ['String'],
226
- result: () => ({ types: ['System.Quantity'], single: true }),
281
+ result: QUANTITY,
227
282
  },
228
283
  convertsToBoolean: { input: { singleton: true }, result: BOOLEAN },
229
284
  convertsToInteger: { input: { singleton: true }, result: BOOLEAN },
@@ -235,9 +290,9 @@ export const FUNCTION_SIGNATURES: Readonly<Record<string, FunctionSignature>> =
235
290
  convertsToTime: { input: { singleton: true }, result: BOOLEAN },
236
291
  convertsToQuantity: { input: { singleton: true }, args: ['String'], result: BOOLEAN },
237
292
 
238
- now: { result: () => ({ types: ['System.DateTime'], single: true }) },
239
- today: { result: () => ({ types: ['System.Date'], single: true }) },
240
- timeOfDay: { result: () => ({ types: ['System.Time'], single: true }) },
293
+ now: { result: DATETIME },
294
+ today: { result: DATE },
295
+ timeOfDay: { result: TIME },
241
296
  yearOf: { input: { kind: 'Temporal', singleton: true }, result: INTEGER },
242
297
  monthOf: { input: { kind: 'Temporal', singleton: true }, result: INTEGER },
243
298
  dayOf: { input: { kind: 'Temporal', singleton: true }, result: INTEGER },
@@ -246,8 +301,8 @@ export const FUNCTION_SIGNATURES: Readonly<Record<string, FunctionSignature>> =
246
301
  secondOf: { input: { kind: 'Temporal', singleton: true }, result: INTEGER },
247
302
  millisecondOf: { input: { kind: 'Temporal', singleton: true }, result: INTEGER },
248
303
  timezoneOffsetOf: { input: { kind: 'Temporal', singleton: true }, result: DECIMAL },
249
- dateOf: { input: { kind: 'Temporal', singleton: true }, result: () => ({ types: ['System.Date'], single: true }) },
250
- timeOf: { input: { kind: 'Temporal', singleton: true }, result: () => ({ types: ['System.Time'], single: true }) },
304
+ dateOf: { input: { kind: 'Temporal', singleton: true }, result: DATE },
305
+ timeOf: { input: { kind: 'Temporal', singleton: true }, result: TIME },
251
306
  lowBoundary: { input: { singleton: true }, args: ['Numeric'], result: UNKNOWN },
252
307
  highBoundary: { input: { singleton: true }, args: ['Numeric'], result: UNKNOWN },
253
308
  precision: { input: { singleton: true }, result: INTEGER },
@@ -255,6 +310,9 @@ export const FUNCTION_SIGNATURES: Readonly<Record<string, FunctionSignature>> =
255
310
  // Variadic: the analyzer repeats the last arg spec for every position, so one
256
311
  // 'expression' entry covers all of coalesce's arguments. The result is the
257
312
  // first non-empty argument, hence the union of all of them.
258
- coalesce: { args: ['expression'], result: (_input, args) => unionStates([...args]) },
313
+ coalesce: { args: ['expression'], result: { kind: 'arguments-union' } },
259
314
  type: { result: UNKNOWN },
260
- }
315
+ } as const satisfies Readonly<Record<string, FunctionSignature>>
316
+
317
+ export type FunctionSignatureName = keyof typeof FUNCTION_SIGNATURE_DEFINITIONS
318
+ export const FUNCTION_SIGNATURES: Readonly<Record<string, FunctionSignature>> = FUNCTION_SIGNATURE_DEFINITIONS
@@ -18,9 +18,18 @@ import type { AstNode } from '../parser/ast.ts'
18
18
  import { parse } from '../parser/parser.ts'
19
19
  import { printExpression } from '../parser/printer.ts'
20
20
  import type { R4TypeOf } from '../r4/generated/type-maps.ts'
21
- import type { FhirpathInput, FhirpathResult, FhirpathResultIn, FhirTypeName } from '../typed/infer.ts'
21
+ import type {
22
+ CheckedFhirpathOptionValues,
23
+ EmptyFhirpathTypeContext,
24
+ FhirpathInput,
25
+ FhirpathResultForContext,
26
+ FhirpathTypeContextOf,
27
+ FhirpathTypeDeclarations,
28
+ FhirTypeName,
29
+ } from '../typed/infer.ts'
22
30
  import { toCollection, type TypedValue, unwrap } from '../values/typed-value.ts'
23
31
  import { LruCache } from './cache.ts'
32
+ import { assertStrictExpression } from './strict.ts'
24
33
 
25
34
  /**
26
35
  * A native or expression-defined FHIRPath function. Native functions receive
@@ -36,12 +45,15 @@ export type SingleCustomFunction =
36
45
  criteria?: never
37
46
  /** Only an expression body reads `%name`; a native `fn` gets plain values. */
38
47
  env?: never
48
+ envTypes?: never
39
49
  })
40
50
  | {
41
51
  expression: AnyExpression
42
52
  signature?: CustomFunctionSignature
43
53
  /** Environment values available only while this expression body runs. */
44
54
  env?: Record<string, unknown>
55
+ /** Explicit types or refinements for this function-local environment. */
56
+ envTypes?: FhirpathTypeDeclarations
45
57
  /** Return one criteria Boolean. An empty body result becomes `false`. */
46
58
  criteria?: boolean
47
59
  fn?: never
@@ -57,6 +69,7 @@ export interface OverloadedCustomFunction {
57
69
  signature?: never
58
70
  criteria?: never
59
71
  env?: never
72
+ envTypes?: never
60
73
  minArity?: never
61
74
  maxArity?: never
62
75
  }
@@ -64,14 +77,20 @@ export interface OverloadedCustomFunction {
64
77
  export type CustomFunction = SingleCustomFunction | OverloadedCustomFunction
65
78
 
66
79
  export interface EvaluateOptions {
80
+ /** Run the static analyzer before evaluation and throw on every error diagnostic. Defaults to false. */
81
+ strict?: boolean
67
82
  /** Environment variables (`%name`), keyed with or without the leading `%`. */
68
83
  env?: Record<string, unknown>
84
+ /** Explicit types or refinements for environment values. */
85
+ envTypes?: FhirpathTypeDeclarations
69
86
  /**
70
87
  * FHIRPath bindings evaluated against the input in declaration order. They
71
88
  * keep FHIRPath types and may use `%context`, `env`, and earlier variables.
72
89
  * During projection they run once per row. A `TypedValue[]` binds directly.
73
90
  */
74
91
  vars?: Record<string, AnyExpression | readonly TypedValue[]>
92
+ /** Types for pre-resolved vars, or explicit overrides for expression vars. */
93
+ varTypes?: FhirpathTypeDeclarations
75
94
  model?: ModelProvider
76
95
  /** Clock for `now()`, `today()`, and `timeOfDay()`. Defaults to the current time. */
77
96
  now?: Date
@@ -91,6 +110,37 @@ export interface EvaluateOptions {
91
110
  regex?: RegexEngine
92
111
  }
93
112
 
113
+ type CheckedOptionKeys<Options, Accepted> = string extends keyof Options
114
+ ? unknown
115
+ : keyof EvaluateOptions extends keyof Options
116
+ ? unknown
117
+ : Record<Exclude<keyof Options, keyof Accepted>, never>
118
+
119
+ /**
120
+ * Keeps literal option declarations for inference and rejects unknown literal
121
+ * keys. Named extensions of the accepted options and index-signature records
122
+ * remain assignable.
123
+ */
124
+ export type Declaring<Options extends object, Accepted extends EvaluateOptions = EvaluateOptions> = Accepted &
125
+ Options &
126
+ CheckedFhirpathOptionValues<Options> &
127
+ CheckedOptionKeys<Options, Accepted>
128
+
129
+ /** Sentinel selecting built-in result inference instead of an explicit TResult. */
130
+ export interface InferredExpressionResult {
131
+ readonly __inferredExpressionResult: unique symbol
132
+ }
133
+
134
+ /** The result of a compiled expression after applying its per-call static declarations. */
135
+ export type CompiledExpressionResult<
136
+ Expr extends string,
137
+ TResult extends unknown[] | InferredExpressionResult,
138
+ Root extends string,
139
+ Options,
140
+ > = TResult extends InferredExpressionResult
141
+ ? FhirpathResultForContext<Expr, Root, FhirpathTypeContextOf<Options>>
142
+ : Extract<TResult, unknown[]>
143
+
94
144
  /**
95
145
  * A parsed expression, reusable across inputs. Create via `compile()` or the
96
146
  * `fhirpath` tag: literal expressions carry inferred result and input types for
@@ -103,9 +153,10 @@ export interface EvaluateOptions {
103
153
  export class CompiledExpression<
104
154
  Expr extends string = string,
105
155
  TInput = FhirpathInput<Expr>,
106
- TResult extends unknown[] = FhirpathResult<Expr>,
156
+ TResult extends unknown[] | InferredExpressionResult = InferredExpressionResult,
157
+ Root extends string = 'opaque',
107
158
  > {
108
- readonly source: string
159
+ readonly source: Expr
109
160
  readonly ast: AstNode
110
161
 
111
162
  constructor(source: Expr) {
@@ -114,13 +165,17 @@ export class CompiledExpression<
114
165
  }
115
166
 
116
167
  /** Evaluate and unwrap results to plain JS values. */
117
- evaluate(input?: TInput, options?: EvaluateOptions): TResult {
118
- return this.evaluateTyped(input, options).map(unwrap) as TResult
168
+ evaluate<const Options extends object = EmptyFhirpathTypeContext>(
169
+ input?: TInput,
170
+ options?: Declaring<Options>
171
+ ): CompiledExpressionResult<Expr, TResult, Root, Options> {
172
+ return this.evaluateTyped(input, options).map(unwrap) as CompiledExpressionResult<Expr, TResult, Root, Options>
119
173
  }
120
174
 
121
175
  /** Evaluate keeping the internal typed representation (types, Decimal, Temporal). */
122
176
  evaluateTyped(input?: unknown, options?: EvaluateOptions): TypedValue[] {
123
177
  const root = toCollection(input)
178
+ assertStrictExpression(this.ast, root, options)
124
179
  return evaluateNode(this.ast, contextFactory(options)(root), root)
125
180
  }
126
181
 
@@ -139,12 +194,12 @@ export class CompiledExpression<
139
194
  export function compile<
140
195
  const Expr extends string,
141
196
  const Root extends FhirTypeName,
142
- TResult extends unknown[] = FhirpathResultIn<Expr, Root>,
143
- >(expression: Expr, inputType: Root): CompiledExpression<Expr, R4TypeOf[Root], TResult>
197
+ TResult extends unknown[] | InferredExpressionResult = InferredExpressionResult,
198
+ >(expression: Expr, inputType: Root): CompiledExpression<Expr, R4TypeOf[Root], TResult, Root>
144
199
  export function compile<
145
200
  const Expr extends string,
146
201
  TInput = FhirpathInput<Expr>,
147
- TResult extends unknown[] = FhirpathResult<Expr>,
202
+ TResult extends unknown[] | InferredExpressionResult = InferredExpressionResult,
148
203
  >(expression: Expr): CompiledExpression<Expr, TInput, TResult>
149
204
  export function compile(expression: string): CompiledExpression {
150
205
  // A declared input type is a compile-time and check-time declaration (see
@@ -152,9 +207,9 @@ export function compile(expression: string): CompiledExpression {
152
207
  return new CompiledExpression(expression)
153
208
  }
154
209
 
155
- /** An expression as text or already compiled, with any literal type. */
156
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any literal-typed CompiledExpression
157
- export type AnyExpression = string | CompiledExpression<any>
210
+ /** An expression as text or already compiled, with any literal, input, result, and root types. */
211
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts every CompiledExpression specialization
212
+ export type AnyExpression = string | CompiledExpression<any, any, any, any>
158
213
 
159
214
  /** A pre-resolved `vars` value; Array.isArray alone cannot exclude readonly arrays for the checker. */
160
215
  function isResolvedCollection<T>(value: T | readonly TypedValue[]): value is readonly TypedValue[] {