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.
- package/README.md +50 -18
- package/dist/analyzer/analyze-dto.d.ts +4 -0
- package/dist/analyzer/analyze-dto.d.ts.map +1 -1
- package/dist/analyzer/analyze-dto.js +12 -14
- package/dist/analyzer/analyze-dto.js.map +1 -1
- package/dist/analyzer/analyze.d.ts +22 -6
- package/dist/analyzer/analyze.d.ts.map +1 -1
- package/dist/analyzer/analyze.js +242 -98
- package/dist/analyzer/analyze.js.map +1 -1
- package/dist/analyzer/declarations.d.ts +26 -0
- package/dist/analyzer/declarations.d.ts.map +1 -0
- package/dist/analyzer/declarations.js +68 -0
- package/dist/analyzer/declarations.js.map +1 -0
- package/dist/analyzer/operator-rules.d.ts +127 -0
- package/dist/analyzer/operator-rules.d.ts.map +1 -0
- package/dist/analyzer/operator-rules.js +70 -0
- package/dist/analyzer/operator-rules.js.map +1 -0
- package/dist/analyzer/signatures.d.ts +1097 -10
- package/dist/analyzer/signatures.d.ts.map +1 -1
- package/dist/analyzer/signatures.js +70 -29
- package/dist/analyzer/signatures.js.map +1 -1
- package/dist/api/compile.d.ts +32 -8
- package/dist/api/compile.d.ts.map +1 -1
- package/dist/api/compile.js +2 -0
- package/dist/api/compile.js.map +1 -1
- package/dist/api/engine.d.ts +42 -16
- package/dist/api/engine.d.ts.map +1 -1
- package/dist/api/engine.js +2 -1
- package/dist/api/engine.js.map +1 -1
- package/dist/api/project.d.ts +8 -8
- package/dist/api/project.d.ts.map +1 -1
- package/dist/api/project.js +31 -19
- package/dist/api/project.js.map +1 -1
- package/dist/api/strict.d.ts +6 -0
- package/dist/api/strict.d.ts.map +1 -0
- package/dist/api/strict.js +71 -0
- package/dist/api/strict.js.map +1 -0
- package/dist/api/tagged.d.ts +4 -4
- package/dist/api/tagged.d.ts.map +1 -1
- package/dist/api/tagged.js.map +1 -1
- package/dist/cli/dto-check.js +7 -3
- package/dist/cli/dto-check.js.map +1 -1
- package/dist/engine/context.d.ts +3 -1
- package/dist/engine/context.d.ts.map +1 -1
- package/dist/engine/context.js +5 -1
- package/dist/engine/context.js.map +1 -1
- package/dist/engine/navigation.d.ts.map +1 -1
- package/dist/engine/navigation.js +7 -48
- package/dist/engine/navigation.js.map +1 -1
- package/dist/engine/type-matching.d.ts.map +1 -1
- package/dist/engine/type-matching.js +6 -16
- package/dist/engine/type-matching.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/parser/parser.d.ts.map +1 -1
- package/dist/parser/parser.js +102 -73
- package/dist/parser/parser.js.map +1 -1
- package/dist/parser/precedence.d.ts +368 -4
- package/dist/parser/precedence.d.ts.map +1 -1
- package/dist/parser/precedence.js +146 -32
- package/dist/parser/precedence.js.map +1 -1
- package/dist/r4/generated/type-maps.d.ts +1367 -0
- package/dist/r4/generated/type-maps.d.ts.map +1 -1
- package/dist/r4/index.d.ts +3 -1
- package/dist/r4/index.d.ts.map +1 -1
- package/dist/typed/context-maps.d.ts +23 -0
- package/dist/typed/context-maps.d.ts.map +1 -0
- package/dist/typed/context-maps.js +2 -0
- package/dist/typed/context-maps.js.map +1 -0
- package/dist/typed/infer.d.ts +99 -229
- package/dist/typed/infer.d.ts.map +1 -1
- package/dist/typed/infer.js +1 -84
- package/dist/typed/infer.js.map +1 -1
- package/dist/typed/inference-limits.d.ts +7 -0
- package/dist/typed/inference-limits.d.ts.map +1 -0
- package/dist/typed/inference-limits.js +6 -0
- package/dist/typed/inference-limits.js.map +1 -0
- package/dist/typed/metadata-compact.d.ts +189 -0
- package/dist/typed/metadata-compact.d.ts.map +1 -0
- package/dist/typed/metadata-compact.js +3 -0
- package/dist/typed/metadata-compact.js.map +1 -0
- package/dist/typed/parser.d.ts +515 -0
- package/dist/typed/parser.d.ts.map +1 -0
- package/dist/typed/parser.js +2 -0
- package/dist/typed/parser.js.map +1 -0
- package/dist/values/type-compat.d.ts +8 -0
- package/dist/values/type-compat.d.ts.map +1 -1
- package/dist/values/type-compat.js +44 -1
- package/dist/values/type-compat.js.map +1 -1
- package/package.json +20 -1
- package/src/analyzer/analyze-dto.ts +21 -17
- package/src/analyzer/analyze.ts +335 -109
- package/src/analyzer/declarations.ts +120 -0
- package/src/analyzer/operator-rules.ts +88 -0
- package/src/analyzer/signatures.ts +99 -41
- package/src/api/compile.ts +66 -11
- package/src/api/engine.ts +97 -33
- package/src/api/project.ts +54 -30
- package/src/api/strict.ts +112 -0
- package/src/api/tagged.ts +5 -5
- package/src/cli/dto-check.ts +7 -3
- package/src/engine/context.ts +7 -2
- package/src/engine/navigation.ts +7 -52
- package/src/engine/type-matching.ts +11 -17
- package/src/index.ts +25 -2
- package/src/parser/parser.ts +106 -74
- package/src/parser/precedence.ts +250 -33
- package/src/r4/generated/type-maps.ts +1557 -0
- package/src/typed/context-maps.ts +41 -0
- package/src/typed/infer.ts +157 -425
- package/src/typed/inference-limits.ts +9 -0
- package/src/typed/metadata-compact.ts +307 -0
- package/src/typed/parser.ts +2277 -0
- package/src/values/type-compat.ts +48 -1
- package/dist/typed/perf-fixture.types.d.ts +0 -136
- package/dist/typed/perf-fixture.types.d.ts.map +0 -1
- package/dist/typed/perf-fixture.types.js +0 -2
- package/dist/typed/perf-fixture.types.js.map +0 -1
- 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 =
|
|
64
|
-
const INTEGER =
|
|
65
|
-
const STRING =
|
|
66
|
-
const DECIMAL =
|
|
67
|
-
const
|
|
68
|
-
const
|
|
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
|
|
105
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
148
|
-
combine: { args: ['any'], result:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
221
|
-
toDateTime: { input: { singleton: true }, result:
|
|
222
|
-
toTime: { input: { singleton: true }, result:
|
|
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:
|
|
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:
|
|
239
|
-
today: { result:
|
|
240
|
-
timeOfDay: { result:
|
|
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:
|
|
250
|
-
timeOf: { input: { kind: 'Temporal', singleton: true }, result:
|
|
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:
|
|
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
|
package/src/api/compile.ts
CHANGED
|
@@ -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 {
|
|
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[] =
|
|
156
|
+
TResult extends unknown[] | InferredExpressionResult = InferredExpressionResult,
|
|
157
|
+
Root extends string = 'opaque',
|
|
107
158
|
> {
|
|
108
|
-
readonly source:
|
|
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
|
|
118
|
-
|
|
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[] =
|
|
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[] =
|
|
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
|
|
156
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts
|
|
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[] {
|