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
@@ -1,22 +1,14 @@
1
1
  import { FhirPathTypeError } from '../errors.ts'
2
- import { resolveByInput, type UnsatisfiedInput, unsatisfiedInput } from '../values/type-compat.ts'
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 SYSTEM_LOCAL_NAMES = new Set([
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' ? SYSTEM_LOCAL_NAMES.has(parts[1] as string) : parts[0] === context.model?.namespace
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 SYSTEM_LOCAL_NAMES.has(name)
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 { EngineInput, EngineOptions, TypedEvaluateOptions } from './api/engine.ts'
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 { FhirpathInput, FhirpathResult, FhirpathResultIn, FhirTypeName } from './typed/infer.ts'
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'
@@ -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 { BindingPower, INFIX_BINDING_POWER } from './precedence.ts'
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 key = token.kind === 'operator' || token.kind === 'punct' || token.kind === 'keyword' ? token.text : ''
51
- const bindingPower = INFIX_BINDING_POWER[key]
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, bindingPower)
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
- return this.parseKeywordPrefix(token)
91
- case 'operator':
92
- return this.parseUnary(token)
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 this.error(`Unexpected '${token.text}'`, token)
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(BindingPower.Unary)
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(token: Token): AstNode {
129
- switch (token.text) {
130
- case '(': {
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, bindingPower: number): AstNode {
188
- switch (token.text) {
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
- default:
199
- break
200
- }
201
- if (TYPE_OPERATORS.has(token.text)) {
202
- const type = this.parseTypeSpecifier()
203
- return {
204
- kind: 'typeOp',
205
- operator: token.text as 'is' | 'as',
206
- operand: left,
207
- type,
208
- span: this.spanBetween(left.span, type.span),
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
- // All FHIRPath binary operators are left-associative, so the right side
212
- // parses at this operator's own binding power.
213
- const right = this.parseExpression(bindingPower)
214
- return {
215
- kind: 'binary',
216
- operator: token.text as BinaryOperator,
217
- left,
218
- right,
219
- span: this.spanBetween(left.span, right.span),
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()
@@ -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
- * (https://hl7.org/fhirpath/#operator-precedence). Higher binds tighter; the
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
- /** Binding power for every infix (and postfix) construct, keyed by token text. */
24
- export const INFIX_BINDING_POWER: Readonly<Record<string, number>> = {
25
- implies: BindingPower.Implies,
26
- or: BindingPower.OrXor,
27
- xor: BindingPower.OrXor,
28
- and: BindingPower.And,
29
- in: BindingPower.Membership,
30
- contains: BindingPower.Membership,
31
- '=': BindingPower.Equality,
32
- '~': BindingPower.Equality,
33
- '!=': BindingPower.Equality,
34
- '!~': BindingPower.Equality,
35
- '<': BindingPower.Comparison,
36
- '>': BindingPower.Comparison,
37
- '<=': BindingPower.Comparison,
38
- '>=': BindingPower.Comparison,
39
- '|': BindingPower.Union,
40
- is: BindingPower.TypeOps,
41
- as: BindingPower.TypeOps,
42
- '+': BindingPower.Additive,
43
- '-': BindingPower.Additive,
44
- '&': BindingPower.Additive,
45
- '*': BindingPower.Multiplicative,
46
- '/': BindingPower.Multiplicative,
47
- div: BindingPower.Multiplicative,
48
- mod: BindingPower.Multiplicative,
49
- '[': BindingPower.Indexer,
50
- '.': BindingPower.Dot,
51
- '(': BindingPower.FunctionCall,
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