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
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
import { FhirPathTypeError } from '../errors.ts'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
resolveByInput,
|
|
4
|
+
SYSTEM_TYPE_LOCAL_NAMES,
|
|
5
|
+
type UnsatisfiedInput,
|
|
6
|
+
unsatisfiedInput,
|
|
7
|
+
} from '../values/type-compat.ts'
|
|
3
8
|
import { OBJECT_TYPE, type TypedValue, typeLocalName } from '../values/typed-value.ts'
|
|
4
9
|
import type { EvaluationContext, HostFunction, HostSingleFunction } from './context.ts'
|
|
5
10
|
|
|
6
|
-
const
|
|
7
|
-
'Any',
|
|
8
|
-
'Boolean',
|
|
9
|
-
'String',
|
|
10
|
-
'Integer',
|
|
11
|
-
'Long',
|
|
12
|
-
'Decimal',
|
|
13
|
-
'Date',
|
|
14
|
-
'DateTime',
|
|
15
|
-
'Time',
|
|
16
|
-
'Quantity',
|
|
17
|
-
])
|
|
18
|
-
|
|
19
|
-
const SYSTEM_LOCAL_NAMES_LOWER = new Set([...SYSTEM_LOCAL_NAMES].map(name => name.toLowerCase()))
|
|
11
|
+
const SYSTEM_LOCAL_NAMES_LOWER = new Set([...SYSTEM_TYPE_LOCAL_NAMES].map(name => name.toLowerCase()))
|
|
20
12
|
|
|
21
13
|
/** True when an unqualified name may mean a System primitive and requires exact `as`/`ofType` matching. */
|
|
22
14
|
function isSystemAmbiguousName(name: string): boolean {
|
|
@@ -126,13 +118,15 @@ function* focusTypes(input: TypedValue[]): Iterable<string> {
|
|
|
126
118
|
/** True when a single-part type name resolves in the model or the System namespace. */
|
|
127
119
|
export function isKnownTypeName(context: EvaluationContext, parts: string[]): boolean {
|
|
128
120
|
if (parts.length === 2) {
|
|
129
|
-
return parts[0] === 'System'
|
|
121
|
+
return parts[0] === 'System'
|
|
122
|
+
? SYSTEM_TYPE_LOCAL_NAMES.has(parts[1] as string)
|
|
123
|
+
: parts[0] === context.model?.namespace
|
|
130
124
|
}
|
|
131
125
|
const name = parts[0] as string
|
|
132
126
|
if (context.model?.resolveType(name) !== undefined) {
|
|
133
127
|
return true
|
|
134
128
|
}
|
|
135
|
-
return
|
|
129
|
+
return SYSTEM_TYPE_LOCAL_NAMES.has(name)
|
|
136
130
|
}
|
|
137
131
|
|
|
138
132
|
function matchesSystemType(item: TypedValue, name: string): boolean {
|
package/src/index.ts
CHANGED
|
@@ -2,8 +2,11 @@ export type { CustomFunctionSignature, ValueArgSpec } from './analyzer/signature
|
|
|
2
2
|
export type { BundleLike } from './api/bundle.ts'
|
|
3
3
|
export type {
|
|
4
4
|
AnyExpression,
|
|
5
|
+
CompiledExpressionResult,
|
|
5
6
|
CustomFunction,
|
|
7
|
+
Declaring,
|
|
6
8
|
EvaluateOptions,
|
|
9
|
+
InferredExpressionResult,
|
|
7
10
|
OverloadedCustomFunction,
|
|
8
11
|
SingleCustomFunction,
|
|
9
12
|
} from './api/compile.ts'
|
|
@@ -11,7 +14,16 @@ export { compile, CompiledExpression, DEFAULT_PARSE_CACHE_SIZE } from './api/com
|
|
|
11
14
|
export type { ConstraintCheckResult, ConstraintIssue, FhirConstraint, OperationOutcome } from './api/constraints.ts'
|
|
12
15
|
export type { ColumnTypeMismatch, DtoBase, DtoClass, DtoEnv, DtoInstance, DtoOptions, DtoRow } from './api/dto.ts'
|
|
13
16
|
export { column, criteria, defineDto } from './api/dto.ts'
|
|
14
|
-
export type {
|
|
17
|
+
export type {
|
|
18
|
+
EngineExpression,
|
|
19
|
+
EngineInput,
|
|
20
|
+
EngineInputRoot,
|
|
21
|
+
EngineOptions,
|
|
22
|
+
EngineProjection,
|
|
23
|
+
EngineProjectionContext,
|
|
24
|
+
EngineResult,
|
|
25
|
+
TypedEvaluateOptions,
|
|
26
|
+
} from './api/engine.ts'
|
|
15
27
|
export { BoundExpression, FhirPathEngine, recordEngines } from './api/engine.ts'
|
|
16
28
|
export { evaluate } from './api/evaluate.ts'
|
|
17
29
|
export type { ColumnOptions, ColumnResult, Projection, ProjectionColumn, ProjectionColumns } from './api/project.ts'
|
|
@@ -23,7 +35,18 @@ export type { ElementInfo, ModelProvider } from './model/provider.ts'
|
|
|
23
35
|
export type { AstNode } from './parser/ast.ts'
|
|
24
36
|
export { parse } from './parser/parser.ts'
|
|
25
37
|
export { printExpression } from './parser/printer.ts'
|
|
26
|
-
export type {
|
|
38
|
+
export type { EmptyContextMap } from './typed/context-maps.ts'
|
|
39
|
+
export type {
|
|
40
|
+
EmptyFhirpathTypeContext,
|
|
41
|
+
FhirpathFunctionDeclaration,
|
|
42
|
+
FhirpathInput,
|
|
43
|
+
FhirpathResult,
|
|
44
|
+
FhirpathResultIn,
|
|
45
|
+
FhirpathTypeContext,
|
|
46
|
+
FhirpathTypeDeclaration,
|
|
47
|
+
FhirpathTypeDeclarations,
|
|
48
|
+
FhirTypeName,
|
|
49
|
+
} from './typed/infer.ts'
|
|
27
50
|
export { Temporal } from './values/datetime.ts'
|
|
28
51
|
export { Decimal } from './values/decimal.ts'
|
|
29
52
|
export type { ValueKind } from './values/type-compat.ts'
|
package/src/parser/parser.ts
CHANGED
|
@@ -5,13 +5,11 @@ import { FhirPathSyntaxError, type SourceSpan } from '../errors.ts'
|
|
|
5
5
|
import { tokenize } from '../lexer/lexer.ts'
|
|
6
6
|
import { CALENDAR_DURATION_UNITS, type Token } from '../lexer/tokens.ts'
|
|
7
7
|
import type { AstNode, BinaryOperator, TypeSpecifier, UnaryOperator } from './ast.ts'
|
|
8
|
-
import {
|
|
8
|
+
import { INFIX_PARSELETS, type InfixParseletRecord, PREFIX_PARSELETS, type PrefixParseletRecord } from './precedence.ts'
|
|
9
9
|
|
|
10
10
|
/** Keywords the grammar also accepts as element names, e.g. `'abc'.contains('b')`. */
|
|
11
11
|
const KEYWORDS_USABLE_AS_IDENTIFIERS: ReadonlySet<string> = new Set(['as', 'contains', 'in', 'is'])
|
|
12
12
|
|
|
13
|
-
const TYPE_OPERATORS: ReadonlySet<string> = new Set(['is', 'as'])
|
|
14
|
-
|
|
15
13
|
/**
|
|
16
14
|
* Nesting far beyond anything a real expression uses. Without a cap, adversarial
|
|
17
15
|
* input like ((((…)))) overflows the call stack as a native RangeError instead of
|
|
@@ -47,13 +45,12 @@ class Parser {
|
|
|
47
45
|
let left = this.parsePrefix()
|
|
48
46
|
for (;;) {
|
|
49
47
|
const token = this.peek()
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
if (bindingPower === undefined || bindingPower <= minBindingPower) {
|
|
48
|
+
const parselet = this.infixParselet(token)
|
|
49
|
+
if (parselet === undefined || parselet.bindingPower <= minBindingPower) {
|
|
53
50
|
return left
|
|
54
51
|
}
|
|
55
52
|
this.advance()
|
|
56
|
-
left = this.parseInfix(left, token,
|
|
53
|
+
left = this.parseInfix(left, token, parselet)
|
|
57
54
|
}
|
|
58
55
|
} finally {
|
|
59
56
|
this.depth--
|
|
@@ -62,9 +59,29 @@ class Parser {
|
|
|
62
59
|
|
|
63
60
|
private parsePrefix(): AstNode {
|
|
64
61
|
const token = this.peek()
|
|
62
|
+
const parselet = this.prefixParselet(token)
|
|
63
|
+
if (parselet === undefined) {
|
|
64
|
+
throw this.error(token.kind === 'end' ? 'Unexpected end of expression' : `Unexpected '${token.text}'`, token)
|
|
65
|
+
}
|
|
66
|
+
switch (parselet.reducer) {
|
|
67
|
+
case 'identifier':
|
|
68
|
+
this.advance()
|
|
69
|
+
return parselet.tokenKind === 'variable'
|
|
70
|
+
? { kind: 'special', name: token.value as 'this' | 'index' | 'total', span: token.span }
|
|
71
|
+
: { kind: 'identifier', name: token.value, span: token.span }
|
|
72
|
+
case 'literal':
|
|
73
|
+
return this.parseLiteral(token)
|
|
74
|
+
case 'unary':
|
|
75
|
+
return this.parseUnary(token, parselet)
|
|
76
|
+
case 'group':
|
|
77
|
+
case 'empty':
|
|
78
|
+
case 'external':
|
|
79
|
+
return this.parsePunctPrefix(token, parselet)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private parseLiteral(token: Token): AstNode {
|
|
65
84
|
switch (token.kind) {
|
|
66
|
-
case 'end':
|
|
67
|
-
throw this.error('Unexpected end of expression', token)
|
|
68
85
|
case 'number':
|
|
69
86
|
return this.parseNumberOrQuantity()
|
|
70
87
|
case 'string':
|
|
@@ -79,44 +96,18 @@ class Parser {
|
|
|
79
96
|
case 'time':
|
|
80
97
|
this.advance()
|
|
81
98
|
return { kind: 'time', text: token.value, span: token.span }
|
|
82
|
-
case 'specialVariable':
|
|
83
|
-
this.advance()
|
|
84
|
-
return { kind: 'special', name: token.value as 'this' | 'index' | 'total', span: token.span }
|
|
85
|
-
case 'identifier':
|
|
86
|
-
case 'delimitedIdentifier':
|
|
87
|
-
this.advance()
|
|
88
|
-
return { kind: 'identifier', name: token.value, span: token.span }
|
|
89
99
|
case 'keyword':
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
case 'punct':
|
|
94
|
-
return this.parsePunctPrefix(token)
|
|
95
|
-
/* v8 ignore start -- exhaustive fallback, every token kind has a case above */
|
|
100
|
+
this.advance()
|
|
101
|
+
return { kind: 'boolean', value: token.text === 'true', span: token.span }
|
|
102
|
+
/* v8 ignore next -- prefixTokenKind admits only literal token kinds here */
|
|
96
103
|
default:
|
|
97
|
-
throw
|
|
98
|
-
/* v8 ignore stop */
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
private parseKeywordPrefix(token: Token): AstNode {
|
|
103
|
-
if (token.text === 'true' || token.text === 'false') {
|
|
104
|
-
this.advance()
|
|
105
|
-
return { kind: 'boolean', value: token.text === 'true', span: token.span }
|
|
106
|
-
}
|
|
107
|
-
if (KEYWORDS_USABLE_AS_IDENTIFIERS.has(token.text)) {
|
|
108
|
-
this.advance()
|
|
109
|
-
return { kind: 'identifier', name: token.value, span: token.span }
|
|
104
|
+
throw new Error(`Invalid literal parselet for '${token.text}'`)
|
|
110
105
|
}
|
|
111
|
-
throw this.error(`Unexpected '${token.text}'`, token)
|
|
112
106
|
}
|
|
113
107
|
|
|
114
|
-
private parseUnary(token: Token): AstNode {
|
|
115
|
-
if (token.text !== '+' && token.text !== '-') {
|
|
116
|
-
throw this.error(`Unexpected '${token.text}'`, token)
|
|
117
|
-
}
|
|
108
|
+
private parseUnary(token: Token, parselet: Extract<PrefixParseletRecord, { reducer: 'unary' }>): AstNode {
|
|
118
109
|
this.advance()
|
|
119
|
-
const operand = this.parseExpression(
|
|
110
|
+
const operand = this.parseExpression(parselet.bindingPower)
|
|
120
111
|
return {
|
|
121
112
|
kind: 'unary',
|
|
122
113
|
operator: token.text as UnaryOperator,
|
|
@@ -125,21 +116,24 @@ class Parser {
|
|
|
125
116
|
}
|
|
126
117
|
}
|
|
127
118
|
|
|
128
|
-
private parsePunctPrefix(
|
|
129
|
-
|
|
130
|
-
|
|
119
|
+
private parsePunctPrefix(
|
|
120
|
+
token: Token,
|
|
121
|
+
parselet: Extract<PrefixParseletRecord, { reducer: 'group' | 'empty' | 'external' }>
|
|
122
|
+
): AstNode {
|
|
123
|
+
switch (parselet.reducer) {
|
|
124
|
+
case 'group': {
|
|
131
125
|
this.advance()
|
|
132
126
|
const inner = this.parseExpression(0)
|
|
133
127
|
this.expect(')')
|
|
134
128
|
this.parenthesized.add(inner)
|
|
135
129
|
return inner
|
|
136
130
|
}
|
|
137
|
-
case '
|
|
131
|
+
case 'empty': {
|
|
138
132
|
this.advance()
|
|
139
133
|
const close = this.expect('}')
|
|
140
134
|
return { kind: 'null', span: this.spanBetween(token.span, close.span) }
|
|
141
135
|
}
|
|
142
|
-
case '
|
|
136
|
+
case 'external': {
|
|
143
137
|
this.advance()
|
|
144
138
|
const name = this.peek()
|
|
145
139
|
if (name.kind !== 'identifier' && name.kind !== 'delimitedIdentifier' && name.kind !== 'string') {
|
|
@@ -148,8 +142,6 @@ class Parser {
|
|
|
148
142
|
this.advance()
|
|
149
143
|
return { kind: 'external', name: name.value, span: this.spanBetween(token.span, name.span) }
|
|
150
144
|
}
|
|
151
|
-
default:
|
|
152
|
-
throw this.error(`Unexpected '${token.text}'`, token)
|
|
153
145
|
}
|
|
154
146
|
}
|
|
155
147
|
|
|
@@ -184,40 +176,52 @@ class Parser {
|
|
|
184
176
|
return { kind: 'number', text: token.value, isDecimal: token.value.includes('.'), span: token.span }
|
|
185
177
|
}
|
|
186
178
|
|
|
187
|
-
private parseInfix(left: AstNode, token: Token,
|
|
188
|
-
switch (
|
|
189
|
-
case '
|
|
179
|
+
private parseInfix(left: AstNode, token: Token, parselet: InfixParseletRecord): AstNode {
|
|
180
|
+
switch (parselet.reducer) {
|
|
181
|
+
case 'dot':
|
|
190
182
|
return this.parseDot(left)
|
|
191
|
-
case '
|
|
183
|
+
case 'indexer': {
|
|
192
184
|
const index = this.parseExpression(0)
|
|
193
185
|
const close = this.expect(']')
|
|
194
186
|
return { kind: 'indexer', target: left, index, span: this.spanBetween(left.span, close.span) }
|
|
195
187
|
}
|
|
196
|
-
case '
|
|
188
|
+
case 'call':
|
|
197
189
|
return this.parseCall(left)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
190
|
+
case 'type': {
|
|
191
|
+
const type = this.parseTypeSpecifier()
|
|
192
|
+
return {
|
|
193
|
+
kind: 'typeOp',
|
|
194
|
+
operator: token.text as 'is' | 'as',
|
|
195
|
+
operand: left,
|
|
196
|
+
type,
|
|
197
|
+
span: this.spanBetween(left.span, type.span),
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
case 'binary': {
|
|
201
|
+
const right = this.parseExpression(parselet.bindingPower)
|
|
202
|
+
return {
|
|
203
|
+
kind: 'binary',
|
|
204
|
+
operator: token.text as BinaryOperator,
|
|
205
|
+
left,
|
|
206
|
+
right,
|
|
207
|
+
span: this.spanBetween(left.span, right.span),
|
|
208
|
+
}
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
return
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private infixParselet(token: Token): InfixParseletRecord | undefined {
|
|
214
|
+
if (token.kind !== 'operator' && token.kind !== 'keyword' && token.kind !== 'punct') return undefined
|
|
215
|
+
const parselet = INFIX_PARSELETS[token.text as keyof typeof INFIX_PARSELETS] as InfixParseletRecord | undefined
|
|
216
|
+
return parselet?.tokenKind === token.kind ? parselet : undefined
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private prefixParselet(token: Token): PrefixParseletRecord | undefined {
|
|
220
|
+
const tokenKind = prefixTokenKind(token)
|
|
221
|
+
if (tokenKind === undefined) return undefined
|
|
222
|
+
const key = tokenKind === 'operator' || tokenKind === 'punct' ? token.text : tokenKind
|
|
223
|
+
const parselet = PREFIX_PARSELETS[key as keyof typeof PREFIX_PARSELETS] as PrefixParseletRecord | undefined
|
|
224
|
+
return parselet?.tokenKind === tokenKind ? parselet : undefined
|
|
221
225
|
}
|
|
222
226
|
|
|
223
227
|
/**
|
|
@@ -315,6 +319,34 @@ class Parser {
|
|
|
315
319
|
}
|
|
316
320
|
}
|
|
317
321
|
|
|
322
|
+
function prefixTokenKind(token: Token): PrefixParseletRecord['tokenKind'] | undefined {
|
|
323
|
+
switch (token.kind) {
|
|
324
|
+
case 'identifier':
|
|
325
|
+
case 'delimitedIdentifier':
|
|
326
|
+
return 'identifier'
|
|
327
|
+
case 'number':
|
|
328
|
+
case 'string':
|
|
329
|
+
case 'date':
|
|
330
|
+
case 'dateTime':
|
|
331
|
+
case 'time':
|
|
332
|
+
return 'literal'
|
|
333
|
+
case 'specialVariable':
|
|
334
|
+
return 'variable'
|
|
335
|
+
case 'operator':
|
|
336
|
+
return 'operator'
|
|
337
|
+
case 'punct':
|
|
338
|
+
return 'punct'
|
|
339
|
+
case 'keyword':
|
|
340
|
+
return token.text === 'true' || token.text === 'false'
|
|
341
|
+
? 'literal'
|
|
342
|
+
: KEYWORDS_USABLE_AS_IDENTIFIERS.has(token.text)
|
|
343
|
+
? 'identifier'
|
|
344
|
+
: undefined
|
|
345
|
+
case 'end':
|
|
346
|
+
return undefined
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
318
350
|
/** Parse a FHIRPath expression into its AST. Throws `FhirPathSyntaxError` with a position on bad input. */
|
|
319
351
|
export function parse(source: string): AstNode {
|
|
320
352
|
return new Parser(source).parse()
|
package/src/parser/precedence.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { BinaryOperator } from './ast.ts'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Binding powers derived from the spec's 13-level operator precedence table
|
|
3
|
-
*
|
|
4
|
-
* spec numbers levels the other way around (1 = `.`, 13 = `implies`).
|
|
4
|
+
* Binding powers derived from the spec's 13-level operator precedence table.
|
|
5
|
+
* Higher binds tighter; the spec numbers levels in the opposite direction.
|
|
5
6
|
*/
|
|
6
7
|
export const BindingPower = {
|
|
7
8
|
Implies: 1,
|
|
@@ -20,33 +21,249 @@ export const BindingPower = {
|
|
|
20
21
|
FunctionCall: 14,
|
|
21
22
|
} as const
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
24
|
+
export type InfixParseletRecord =
|
|
25
|
+
| {
|
|
26
|
+
tokenKind: 'operator' | 'keyword'
|
|
27
|
+
fixity: 'infix'
|
|
28
|
+
bindingPower: number
|
|
29
|
+
associativity: 'left'
|
|
30
|
+
rhs: 'expression'
|
|
31
|
+
reducer: 'binary'
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
tokenKind: 'keyword'
|
|
35
|
+
fixity: 'infix'
|
|
36
|
+
bindingPower: number
|
|
37
|
+
associativity: 'left'
|
|
38
|
+
rhs: 'type-name'
|
|
39
|
+
reducer: 'type'
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
tokenKind: 'punct'
|
|
43
|
+
fixity: 'infix'
|
|
44
|
+
bindingPower: number
|
|
45
|
+
associativity: 'left'
|
|
46
|
+
rhs: 'path'
|
|
47
|
+
reducer: 'dot'
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
tokenKind: 'punct'
|
|
51
|
+
fixity: 'postfix'
|
|
52
|
+
bindingPower: number
|
|
53
|
+
associativity: 'left'
|
|
54
|
+
rhs: 'index'
|
|
55
|
+
reducer: 'indexer'
|
|
56
|
+
}
|
|
57
|
+
| {
|
|
58
|
+
tokenKind: 'punct'
|
|
59
|
+
fixity: 'postfix'
|
|
60
|
+
bindingPower: number
|
|
61
|
+
associativity: 'left'
|
|
62
|
+
rhs: 'arguments'
|
|
63
|
+
reducer: 'call'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type PrefixParseletRecord =
|
|
67
|
+
| {
|
|
68
|
+
tokenKind: 'identifier' | 'variable'
|
|
69
|
+
fixity: 'prefix'
|
|
70
|
+
bindingPower: number
|
|
71
|
+
associativity: 'none'
|
|
72
|
+
rhs: 'none'
|
|
73
|
+
reducer: 'identifier'
|
|
74
|
+
}
|
|
75
|
+
| {
|
|
76
|
+
tokenKind: 'literal'
|
|
77
|
+
fixity: 'prefix'
|
|
78
|
+
bindingPower: number
|
|
79
|
+
associativity: 'none'
|
|
80
|
+
rhs: 'none'
|
|
81
|
+
reducer: 'literal'
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
tokenKind: 'operator'
|
|
85
|
+
fixity: 'prefix'
|
|
86
|
+
bindingPower: number
|
|
87
|
+
associativity: 'right'
|
|
88
|
+
rhs: 'expression'
|
|
89
|
+
reducer: 'unary'
|
|
90
|
+
}
|
|
91
|
+
| {
|
|
92
|
+
tokenKind: 'punct'
|
|
93
|
+
fixity: 'prefix'
|
|
94
|
+
bindingPower: number
|
|
95
|
+
associativity: 'none'
|
|
96
|
+
rhs: 'group'
|
|
97
|
+
reducer: 'group'
|
|
98
|
+
}
|
|
99
|
+
| {
|
|
100
|
+
tokenKind: 'punct'
|
|
101
|
+
fixity: 'prefix'
|
|
102
|
+
bindingPower: number
|
|
103
|
+
associativity: 'none'
|
|
104
|
+
rhs: 'none'
|
|
105
|
+
reducer: 'empty'
|
|
106
|
+
}
|
|
107
|
+
| {
|
|
108
|
+
tokenKind: 'punct'
|
|
109
|
+
fixity: 'prefix'
|
|
110
|
+
bindingPower: number
|
|
111
|
+
associativity: 'none'
|
|
112
|
+
rhs: 'external-name'
|
|
113
|
+
reducer: 'external'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Single source of truth for tokens and token kinds accepted before an operand. */
|
|
117
|
+
export const PREFIX_PARSELETS = {
|
|
118
|
+
identifier: {
|
|
119
|
+
tokenKind: 'identifier',
|
|
120
|
+
fixity: 'prefix',
|
|
121
|
+
bindingPower: 0,
|
|
122
|
+
associativity: 'none',
|
|
123
|
+
rhs: 'none',
|
|
124
|
+
reducer: 'identifier',
|
|
125
|
+
},
|
|
126
|
+
literal: {
|
|
127
|
+
tokenKind: 'literal',
|
|
128
|
+
fixity: 'prefix',
|
|
129
|
+
bindingPower: 0,
|
|
130
|
+
associativity: 'none',
|
|
131
|
+
rhs: 'none',
|
|
132
|
+
reducer: 'literal',
|
|
133
|
+
},
|
|
134
|
+
variable: {
|
|
135
|
+
tokenKind: 'variable',
|
|
136
|
+
fixity: 'prefix',
|
|
137
|
+
bindingPower: 0,
|
|
138
|
+
associativity: 'none',
|
|
139
|
+
rhs: 'none',
|
|
140
|
+
reducer: 'identifier',
|
|
141
|
+
},
|
|
142
|
+
'+': {
|
|
143
|
+
tokenKind: 'operator',
|
|
144
|
+
fixity: 'prefix',
|
|
145
|
+
bindingPower: BindingPower.Unary,
|
|
146
|
+
associativity: 'right',
|
|
147
|
+
rhs: 'expression',
|
|
148
|
+
reducer: 'unary',
|
|
149
|
+
},
|
|
150
|
+
'-': {
|
|
151
|
+
tokenKind: 'operator',
|
|
152
|
+
fixity: 'prefix',
|
|
153
|
+
bindingPower: BindingPower.Unary,
|
|
154
|
+
associativity: 'right',
|
|
155
|
+
rhs: 'expression',
|
|
156
|
+
reducer: 'unary',
|
|
157
|
+
},
|
|
158
|
+
'(': {
|
|
159
|
+
tokenKind: 'punct',
|
|
160
|
+
fixity: 'prefix',
|
|
161
|
+
bindingPower: 0,
|
|
162
|
+
associativity: 'none',
|
|
163
|
+
rhs: 'group',
|
|
164
|
+
reducer: 'group',
|
|
165
|
+
},
|
|
166
|
+
'{': {
|
|
167
|
+
tokenKind: 'punct',
|
|
168
|
+
fixity: 'prefix',
|
|
169
|
+
bindingPower: 0,
|
|
170
|
+
associativity: 'none',
|
|
171
|
+
rhs: 'none',
|
|
172
|
+
reducer: 'empty',
|
|
173
|
+
},
|
|
174
|
+
'%': {
|
|
175
|
+
tokenKind: 'punct',
|
|
176
|
+
fixity: 'prefix',
|
|
177
|
+
bindingPower: 0,
|
|
178
|
+
associativity: 'none',
|
|
179
|
+
rhs: 'external-name',
|
|
180
|
+
reducer: 'external',
|
|
181
|
+
},
|
|
182
|
+
} as const satisfies Record<string, PrefixParseletRecord>
|
|
183
|
+
|
|
184
|
+
const binary = (
|
|
185
|
+
tokenKind: 'operator' | 'keyword',
|
|
186
|
+
bindingPower: number
|
|
187
|
+
): Extract<InfixParseletRecord, { reducer: 'binary' }> => ({
|
|
188
|
+
tokenKind,
|
|
189
|
+
fixity: 'infix',
|
|
190
|
+
bindingPower,
|
|
191
|
+
associativity: 'left',
|
|
192
|
+
rhs: 'expression',
|
|
193
|
+
reducer: 'binary',
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
/** Single source of truth for every token accepted after an operand. */
|
|
197
|
+
export const INFIX_PARSELETS = {
|
|
198
|
+
implies: binary('keyword', BindingPower.Implies),
|
|
199
|
+
or: binary('keyword', BindingPower.OrXor),
|
|
200
|
+
xor: binary('keyword', BindingPower.OrXor),
|
|
201
|
+
and: binary('keyword', BindingPower.And),
|
|
202
|
+
in: binary('keyword', BindingPower.Membership),
|
|
203
|
+
contains: binary('keyword', BindingPower.Membership),
|
|
204
|
+
'=': binary('operator', BindingPower.Equality),
|
|
205
|
+
'~': binary('operator', BindingPower.Equality),
|
|
206
|
+
'!=': binary('operator', BindingPower.Equality),
|
|
207
|
+
'!~': binary('operator', BindingPower.Equality),
|
|
208
|
+
'<': binary('operator', BindingPower.Comparison),
|
|
209
|
+
'>': binary('operator', BindingPower.Comparison),
|
|
210
|
+
'<=': binary('operator', BindingPower.Comparison),
|
|
211
|
+
'>=': binary('operator', BindingPower.Comparison),
|
|
212
|
+
'|': binary('operator', BindingPower.Union),
|
|
213
|
+
is: {
|
|
214
|
+
tokenKind: 'keyword',
|
|
215
|
+
fixity: 'infix',
|
|
216
|
+
bindingPower: BindingPower.TypeOps,
|
|
217
|
+
associativity: 'left',
|
|
218
|
+
rhs: 'type-name',
|
|
219
|
+
reducer: 'type',
|
|
220
|
+
},
|
|
221
|
+
as: {
|
|
222
|
+
tokenKind: 'keyword',
|
|
223
|
+
fixity: 'infix',
|
|
224
|
+
bindingPower: BindingPower.TypeOps,
|
|
225
|
+
associativity: 'left',
|
|
226
|
+
rhs: 'type-name',
|
|
227
|
+
reducer: 'type',
|
|
228
|
+
},
|
|
229
|
+
'+': binary('operator', BindingPower.Additive),
|
|
230
|
+
'-': binary('operator', BindingPower.Additive),
|
|
231
|
+
'&': binary('operator', BindingPower.Additive),
|
|
232
|
+
'*': binary('operator', BindingPower.Multiplicative),
|
|
233
|
+
'/': binary('operator', BindingPower.Multiplicative),
|
|
234
|
+
div: binary('keyword', BindingPower.Multiplicative),
|
|
235
|
+
mod: binary('keyword', BindingPower.Multiplicative),
|
|
236
|
+
'[': {
|
|
237
|
+
tokenKind: 'punct',
|
|
238
|
+
fixity: 'postfix',
|
|
239
|
+
bindingPower: BindingPower.Indexer,
|
|
240
|
+
associativity: 'left',
|
|
241
|
+
rhs: 'index',
|
|
242
|
+
reducer: 'indexer',
|
|
243
|
+
},
|
|
244
|
+
'.': {
|
|
245
|
+
tokenKind: 'punct',
|
|
246
|
+
fixity: 'infix',
|
|
247
|
+
bindingPower: BindingPower.Dot,
|
|
248
|
+
associativity: 'left',
|
|
249
|
+
rhs: 'path',
|
|
250
|
+
reducer: 'dot',
|
|
251
|
+
},
|
|
252
|
+
'(': {
|
|
253
|
+
tokenKind: 'punct',
|
|
254
|
+
fixity: 'postfix',
|
|
255
|
+
bindingPower: BindingPower.FunctionCall,
|
|
256
|
+
associativity: 'left',
|
|
257
|
+
rhs: 'arguments',
|
|
258
|
+
reducer: 'call',
|
|
259
|
+
},
|
|
260
|
+
} as const satisfies Record<string, InfixParseletRecord>
|
|
261
|
+
|
|
262
|
+
/** Compatibility view used by callers that only need precedence. */
|
|
263
|
+
export const INFIX_BINDING_POWER: Readonly<Record<string, number>> = Object.fromEntries(
|
|
264
|
+
Object.entries(INFIX_PARSELETS).map(([token, record]) => [token, record.bindingPower])
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
/** Compile-time coverage: every binary AST operator has a binary parselet. */
|
|
268
|
+
const _binaryOperatorCoverage: Record<BinaryOperator, InfixParseletRecord> = INFIX_PARSELETS
|
|
269
|
+
void _binaryOperatorCoverage
|