@tamagui/compiler-core 0.0.0-bootstrap.0 → 3.0.0-beta.1093.1

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 (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -1
  3. package/dist/cjs/ast.cjs +108 -0
  4. package/dist/cjs/contracts.cjs +61 -0
  5. package/dist/cjs/diagnostics.cjs +68 -0
  6. package/dist/cjs/evaluate.cjs +602 -0
  7. package/dist/cjs/graph.cjs +335 -0
  8. package/dist/cjs/hash.cjs +48 -0
  9. package/dist/cjs/index.cjs +43 -0
  10. package/dist/cjs/ir.cjs +32 -0
  11. package/dist/cjs/lower.cjs +232 -0
  12. package/dist/cjs/materialize.cjs +222 -0
  13. package/dist/cjs/normalize.cjs +628 -0
  14. package/dist/cjs/output.cjs +148 -0
  15. package/dist/cjs/planCache.cjs +262 -0
  16. package/dist/cjs/session.cjs +127 -0
  17. package/dist/cjs/yuku.cjs +166 -0
  18. package/dist/cjs/zero.cjs +389 -0
  19. package/dist/esm/ast.mjs +79 -0
  20. package/dist/esm/ast.mjs.map +1 -0
  21. package/dist/esm/contracts.mjs +34 -0
  22. package/dist/esm/contracts.mjs.map +1 -0
  23. package/dist/esm/diagnostics.mjs +44 -0
  24. package/dist/esm/diagnostics.mjs.map +1 -0
  25. package/dist/esm/evaluate.mjs +576 -0
  26. package/dist/esm/evaluate.mjs.map +1 -0
  27. package/dist/esm/graph.mjs +313 -0
  28. package/dist/esm/graph.mjs.map +1 -0
  29. package/dist/esm/hash.mjs +26 -0
  30. package/dist/esm/hash.mjs.map +1 -0
  31. package/dist/esm/index.mjs +30 -0
  32. package/dist/esm/ir.mjs +12 -0
  33. package/dist/esm/ir.mjs.map +1 -0
  34. package/dist/esm/lower.mjs +210 -0
  35. package/dist/esm/lower.mjs.map +1 -0
  36. package/dist/esm/materialize.mjs +203 -0
  37. package/dist/esm/materialize.mjs.map +1 -0
  38. package/dist/esm/normalize.mjs +604 -0
  39. package/dist/esm/normalize.mjs.map +1 -0
  40. package/dist/esm/output.mjs +119 -0
  41. package/dist/esm/output.mjs.map +1 -0
  42. package/dist/esm/planCache.mjs +232 -0
  43. package/dist/esm/planCache.mjs.map +1 -0
  44. package/dist/esm/session.mjs +108 -0
  45. package/dist/esm/session.mjs.map +1 -0
  46. package/dist/esm/yuku.mjs +144 -0
  47. package/dist/esm/yuku.mjs.map +1 -0
  48. package/dist/esm/zero.mjs +354 -0
  49. package/dist/esm/zero.mjs.map +1 -0
  50. package/package.json +42 -4
  51. package/src/ast.ts +106 -0
  52. package/src/contracts.ts +150 -0
  53. package/src/diagnostics.ts +96 -0
  54. package/src/evaluate.ts +875 -0
  55. package/src/graph.ts +425 -0
  56. package/src/hash.ts +36 -0
  57. package/src/index.ts +15 -0
  58. package/src/ir.ts +156 -0
  59. package/src/lower.ts +435 -0
  60. package/src/materialize.ts +383 -0
  61. package/src/normalize.ts +834 -0
  62. package/src/output.ts +175 -0
  63. package/src/planCache.ts +362 -0
  64. package/src/session.ts +192 -0
  65. package/src/yuku.ts +197 -0
  66. package/src/zero.ts +609 -0
  67. package/types/ast.d.ts +11 -0
  68. package/types/ast.d.ts.map +1 -0
  69. package/types/contracts.d.ts +99 -0
  70. package/types/contracts.d.ts.map +1 -0
  71. package/types/diagnostics.d.ts +31 -0
  72. package/types/diagnostics.d.ts.map +1 -0
  73. package/types/evaluate.d.ts +48 -0
  74. package/types/evaluate.d.ts.map +1 -0
  75. package/types/graph.d.ts +51 -0
  76. package/types/graph.d.ts.map +1 -0
  77. package/types/hash.d.ts +8 -0
  78. package/types/hash.d.ts.map +1 -0
  79. package/types/index.d.ts +16 -0
  80. package/types/index.d.ts.map +1 -0
  81. package/types/ir.d.ts +120 -0
  82. package/types/ir.d.ts.map +1 -0
  83. package/types/lower.d.ts +99 -0
  84. package/types/lower.d.ts.map +1 -0
  85. package/types/materialize.d.ts +121 -0
  86. package/types/materialize.d.ts.map +1 -0
  87. package/types/normalize.d.ts +7 -0
  88. package/types/normalize.d.ts.map +1 -0
  89. package/types/output.d.ts +32 -0
  90. package/types/output.d.ts.map +1 -0
  91. package/types/planCache.d.ts +123 -0
  92. package/types/planCache.d.ts.map +1 -0
  93. package/types/session.d.ts +52 -0
  94. package/types/session.d.ts.map +1 -0
  95. package/types/yuku.d.ts +4 -0
  96. package/types/yuku.d.ts.map +1 -0
  97. package/types/zero.d.ts +99 -0
  98. package/types/zero.d.ts.map +1 -0
@@ -0,0 +1,875 @@
1
+ import { childNode, childNodes, identifierName, isAstNode, unwrapExpression } from './ast'
2
+ import type {
3
+ AstNode,
4
+ ExpressionReference,
5
+ ResolvedModuleId,
6
+ SourceSpan,
7
+ SymbolDefinition,
8
+ SymbolResolver,
9
+ } from './contracts'
10
+ import { expressionReference, spanOf } from './contracts'
11
+ import { linkedBailout, localBailout, type BailoutReason } from './diagnostics'
12
+
13
+ export type StaticEvaluationValue =
14
+ | string
15
+ | number
16
+ | boolean
17
+ | null
18
+ | StaticEvaluationValue[]
19
+ | { [key: string]: StaticEvaluationValue }
20
+
21
+ export type EvaluationResult =
22
+ | {
23
+ ok: true
24
+ value: StaticEvaluationValue
25
+ dependencies: ResolvedModuleId[]
26
+ }
27
+ | {
28
+ ok: false
29
+ bailout: BailoutReason
30
+ }
31
+
32
+ export interface DynamicEvaluation {
33
+ type: 'number' | 'string' | 'boolean' | 'null'
34
+ values?: (number | string | boolean | null)[]
35
+ dependencies: ResolvedModuleId[]
36
+ }
37
+
38
+ type InternalResult =
39
+ | { ok: true; value: StaticEvaluationValue }
40
+ | { ok: false; bailout: BailoutReason }
41
+
42
+ interface EvaluationState {
43
+ activeDefinitions: Set<string>
44
+ dependencies: Set<ResolvedModuleId>
45
+ }
46
+
47
+ function success(value: StaticEvaluationValue): InternalResult {
48
+ return { ok: true, value }
49
+ }
50
+
51
+ function literal(node: AstNode): StaticEvaluationValue | undefined {
52
+ if (node.type === 'NullLiteral') return null
53
+ if (node.type === 'Literal' || node.type.endsWith('Literal')) {
54
+ const value = node.value
55
+ if (
56
+ typeof value === 'string' ||
57
+ typeof value === 'number' ||
58
+ typeof value === 'boolean' ||
59
+ value === null
60
+ ) {
61
+ return value
62
+ }
63
+ }
64
+ return undefined
65
+ }
66
+
67
+ function unsupported(
68
+ id: ResolvedModuleId,
69
+ node: AstNode,
70
+ detail = `Expression ${node.type} is not statically supported`
71
+ ): InternalResult {
72
+ return {
73
+ ok: false,
74
+ bailout: localBailout('local/unsupported-expression', spanOf(id, node), detail),
75
+ }
76
+ }
77
+
78
+ function evaluateDefinition(
79
+ resolver: SymbolResolver,
80
+ definition: SymbolDefinition,
81
+ useSpan: SourceSpan,
82
+ state: EvaluationState
83
+ ): InternalResult {
84
+ const key = `${definition.id}:${definition.span.start}`
85
+ if (state.activeDefinitions.has(key)) {
86
+ return {
87
+ ok: false,
88
+ bailout: localBailout(
89
+ 'local/static-evaluation-cycle',
90
+ useSpan,
91
+ `Static evaluation cycle reached ${definition.name}`
92
+ ),
93
+ }
94
+ }
95
+ if (!definition.initializer) {
96
+ return {
97
+ ok: false,
98
+ bailout: linkedBailout(
99
+ 'linked/missing-initializer',
100
+ useSpan,
101
+ `Binding ${definition.name} has no static initializer`,
102
+ definition.id
103
+ ),
104
+ }
105
+ }
106
+ if (!definition.constant) {
107
+ return {
108
+ ok: false,
109
+ bailout: linkedBailout(
110
+ 'linked/unresolved-binding',
111
+ useSpan,
112
+ `Binding ${definition.name} is mutable and cannot be statically evaluated`,
113
+ definition.id
114
+ ),
115
+ }
116
+ }
117
+ const initializer = resolver.expressionNode(definition.initializer)
118
+ if (!initializer) {
119
+ return {
120
+ ok: false,
121
+ bailout: linkedBailout(
122
+ 'linked/missing-initializer',
123
+ useSpan,
124
+ `Binding ${definition.name} initializer is outside the linked graph`,
125
+ definition.id
126
+ ),
127
+ }
128
+ }
129
+
130
+ state.dependencies.add(definition.id)
131
+ state.activeDefinitions.add(key)
132
+ try {
133
+ return evaluateNode(resolver, definition.id, initializer, state)
134
+ } finally {
135
+ state.activeDefinitions.delete(key)
136
+ }
137
+ }
138
+
139
+ function evaluateObject(
140
+ resolver: SymbolResolver,
141
+ id: ResolvedModuleId,
142
+ node: AstNode,
143
+ state: EvaluationState
144
+ ): InternalResult {
145
+ const output: Record<string, StaticEvaluationValue> = {}
146
+ for (const property of childNodes(node, 'properties')) {
147
+ if (property.type === 'SpreadElement') {
148
+ const argument = childNode(property, 'argument')
149
+ if (!argument) return unsupported(id, property)
150
+ const spread = evaluateNode(resolver, id, argument, state)
151
+ if (!spread.ok) return spread
152
+ if (
153
+ !spread.value ||
154
+ typeof spread.value !== 'object' ||
155
+ Array.isArray(spread.value)
156
+ ) {
157
+ return {
158
+ ok: false,
159
+ bailout: localBailout(
160
+ 'local/non-object-spread',
161
+ spanOf(id, property),
162
+ 'Static object spread did not evaluate to an object'
163
+ ),
164
+ }
165
+ }
166
+ Object.assign(output, spread.value)
167
+ continue
168
+ }
169
+ if (property.type !== 'Property' && property.type !== 'ObjectProperty') {
170
+ return unsupported(id, property)
171
+ }
172
+ const keyNode = childNode(property, 'key')
173
+ const valueNode = childNode(property, 'value')
174
+ if (!keyNode || !valueNode) return unsupported(id, property)
175
+ let key: StaticEvaluationValue | undefined = identifierName(keyNode) ?? undefined
176
+ if (property.computed === true || key === undefined) {
177
+ const evaluatedKey = evaluateNode(resolver, id, keyNode, state)
178
+ if (!evaluatedKey.ok) return evaluatedKey
179
+ key = evaluatedKey.value
180
+ }
181
+ if (typeof key !== 'string' && typeof key !== 'number') {
182
+ return unsupported(id, keyNode, 'Static object key is not a string or number')
183
+ }
184
+ const value = evaluateNode(resolver, id, valueNode, state)
185
+ if (!value.ok) return value
186
+ output[String(key)] = value.value
187
+ }
188
+ return success(output)
189
+ }
190
+
191
+ function evaluateArray(
192
+ resolver: SymbolResolver,
193
+ id: ResolvedModuleId,
194
+ node: AstNode,
195
+ state: EvaluationState
196
+ ): InternalResult {
197
+ const output: StaticEvaluationValue[] = []
198
+ const rawElements = node.elements
199
+ if (
200
+ !Array.isArray(rawElements) ||
201
+ rawElements.some((element) => element === null || !isAstNode(element))
202
+ ) {
203
+ return unsupported(id, node, 'Sparse arrays are not statically supported')
204
+ }
205
+ for (const element of rawElements.filter(isAstNode)) {
206
+ if (element.type === 'SpreadElement') {
207
+ const argument = childNode(element, 'argument')
208
+ if (!argument) return unsupported(id, element)
209
+ const spread = evaluateNode(resolver, id, argument, state)
210
+ if (!spread.ok) return spread
211
+ if (!Array.isArray(spread.value)) {
212
+ return unsupported(
213
+ id,
214
+ element,
215
+ 'Static array spread did not evaluate to an array'
216
+ )
217
+ }
218
+ output.push(...spread.value)
219
+ continue
220
+ }
221
+ const value = evaluateNode(resolver, id, element, state)
222
+ if (!value.ok) return value
223
+ output.push(value.value)
224
+ }
225
+ return success(output)
226
+ }
227
+
228
+ function evaluateNode(
229
+ resolver: SymbolResolver,
230
+ id: ResolvedModuleId,
231
+ input: AstNode,
232
+ state: EvaluationState
233
+ ): InternalResult {
234
+ const node = unwrapExpression(input)
235
+ const literalValue = literal(node)
236
+ if (literalValue !== undefined) return success(literalValue)
237
+
238
+ switch (node.type) {
239
+ case 'Identifier': {
240
+ const name = identifierName(node)
241
+ if (!name) return unsupported(id, node)
242
+ // by position, so a function-scope constant resolves and a parameter
243
+ // shadowing a module constant does not read the module's value
244
+ const definition = resolver.resolveReference(expressionReference(id, node))
245
+ if (!definition) {
246
+ return {
247
+ ok: false,
248
+ bailout: linkedBailout(
249
+ 'linked/unresolved-binding',
250
+ spanOf(id, node),
251
+ `Binding ${name} has no host-linked definition`
252
+ ),
253
+ }
254
+ }
255
+ return evaluateDefinition(resolver, definition, spanOf(id, node), state)
256
+ }
257
+ case 'ObjectExpression':
258
+ return evaluateObject(resolver, id, node, state)
259
+ case 'ArrayExpression':
260
+ return evaluateArray(resolver, id, node, state)
261
+ case 'MemberExpression': {
262
+ const objectNode = childNode(node, 'object')
263
+ const propertyNode = childNode(node, 'property')
264
+ if (!objectNode || !propertyNode) return unsupported(id, node)
265
+ const object = evaluateNode(resolver, id, objectNode, state)
266
+ if (!object.ok) return object
267
+ let property: StaticEvaluationValue | null = identifierName(propertyNode)
268
+ if (node.computed === true || property === null) {
269
+ const evaluatedProperty = evaluateNode(resolver, id, propertyNode, state)
270
+ if (!evaluatedProperty.ok) return evaluatedProperty
271
+ property = evaluatedProperty.value
272
+ }
273
+ if (
274
+ (typeof object.value !== 'object' || object.value === null) &&
275
+ typeof object.value !== 'string'
276
+ ) {
277
+ return unsupported(id, node, 'Static member target is not indexable')
278
+ }
279
+ if (typeof property !== 'string' && typeof property !== 'number') {
280
+ return unsupported(
281
+ id,
282
+ propertyNode,
283
+ 'Static member key is not a string or number'
284
+ )
285
+ }
286
+ const value = (object.value as Record<string, StaticEvaluationValue>)[
287
+ String(property)
288
+ ]
289
+ return value === undefined
290
+ ? unsupported(id, node, `Static member ${String(property)} does not exist`)
291
+ : success(value)
292
+ }
293
+ case 'UnaryExpression': {
294
+ const argument = childNode(node, 'argument')
295
+ if (!argument) return unsupported(id, node)
296
+ const value = evaluateNode(resolver, id, argument, state)
297
+ if (!value.ok) return value
298
+ if (node.operator === '-' && typeof value.value === 'number') {
299
+ return success(-value.value)
300
+ }
301
+ if (node.operator === '+' && typeof value.value === 'number') {
302
+ return success(value.value)
303
+ }
304
+ if (node.operator === '!') return success(!value.value)
305
+ return unsupported(
306
+ id,
307
+ node,
308
+ `Unary operator ${String(node.operator)} is unsupported`
309
+ )
310
+ }
311
+ case 'BinaryExpression':
312
+ case 'LogicalExpression': {
313
+ const leftNode = childNode(node, 'left')
314
+ const rightNode = childNode(node, 'right')
315
+ if (!leftNode || !rightNode) return unsupported(id, node)
316
+ const left = evaluateNode(resolver, id, leftNode, state)
317
+ if (!left.ok) return left
318
+ if (node.type === 'LogicalExpression') {
319
+ if (node.operator === '&&' && !left.value) return left
320
+ if (node.operator === '||' && left.value) return left
321
+ if (node.operator === '??' && left.value !== null) return left
322
+ }
323
+ const right = evaluateNode(resolver, id, rightNode, state)
324
+ if (!right.ok) return right
325
+ switch (node.operator) {
326
+ case '+':
327
+ if (typeof left.value === 'number' && typeof right.value === 'number') {
328
+ return success(left.value + right.value)
329
+ }
330
+ if (typeof left.value === 'string' || typeof right.value === 'string') {
331
+ return success(String(left.value) + String(right.value))
332
+ }
333
+ break
334
+ case '-':
335
+ if (typeof left.value === 'number' && typeof right.value === 'number') {
336
+ return success(left.value - right.value)
337
+ }
338
+ break
339
+ case '*':
340
+ if (typeof left.value === 'number' && typeof right.value === 'number') {
341
+ return success(left.value * right.value)
342
+ }
343
+ break
344
+ case '/':
345
+ if (typeof left.value === 'number' && typeof right.value === 'number') {
346
+ return success(left.value / right.value)
347
+ }
348
+ break
349
+ case '&&':
350
+ case '||':
351
+ case '??':
352
+ return right
353
+ }
354
+ return unsupported(
355
+ id,
356
+ node,
357
+ `Binary operator ${String(node.operator)} is unsupported`
358
+ )
359
+ }
360
+ case 'ConditionalExpression': {
361
+ const testNode = childNode(node, 'test')
362
+ const consequent = childNode(node, 'consequent')
363
+ const alternate = childNode(node, 'alternate')
364
+ if (!testNode || !consequent || !alternate) return unsupported(id, node)
365
+ const test = evaluateNode(resolver, id, testNode, state)
366
+ if (!test.ok) return test
367
+ return evaluateNode(resolver, id, test.value ? consequent : alternate, state)
368
+ }
369
+ case 'TemplateLiteral': {
370
+ const quasis = childNodes(node, 'quasis')
371
+ const expressions = childNodes(node, 'expressions')
372
+ let output = ''
373
+ for (let index = 0; index < quasis.length; index++) {
374
+ output += quasiText(quasis[index])
375
+ const expression = expressions[index]
376
+ if (expression) {
377
+ const value = evaluateNode(resolver, id, expression, state)
378
+ if (!value.ok) return value
379
+ output += String(value.value)
380
+ }
381
+ }
382
+ return success(output)
383
+ }
384
+ default:
385
+ return unsupported(id, node)
386
+ }
387
+ }
388
+
389
+ function quasiText(quasi: AstNode | undefined): string {
390
+ const cooked = (quasi?.value as { cooked?: unknown } | undefined)?.cooked
391
+ const raw = (quasi?.value as { raw?: unknown } | undefined)?.raw
392
+ return typeof cooked === 'string' ? cooked : typeof raw === 'string' ? raw : ''
393
+ }
394
+
395
+ function dynamicPrimitive(
396
+ value: StaticEvaluationValue
397
+ ): DynamicEvaluation['type'] | null {
398
+ if (value === null) return 'null'
399
+ if (typeof value === 'number') return 'number'
400
+ if (typeof value === 'string') return 'string'
401
+ if (typeof value === 'boolean') return 'boolean'
402
+ return null
403
+ }
404
+
405
+ function evaluateDynamicNode(
406
+ resolver: SymbolResolver,
407
+ id: ResolvedModuleId,
408
+ input: AstNode,
409
+ state: EvaluationState
410
+ ): Omit<DynamicEvaluation, 'dependencies'> | null {
411
+ const exact = evaluateNode(resolver, id, input, state)
412
+ if (exact.ok) {
413
+ const type = dynamicPrimitive(exact.value)
414
+ return type
415
+ ? { type, values: [exact.value as number | string | boolean | null] }
416
+ : null
417
+ }
418
+
419
+ const node = unwrapExpression(input)
420
+ if (node.type === 'Identifier') {
421
+ if (!identifierName(node)) return null
422
+ const definition = resolver.resolveReference(expressionReference(id, node))
423
+ if (!definition?.constant || !definition.initializer) return null
424
+ const key = `${definition.id}:${definition.span.start}`
425
+ if (state.activeDefinitions.has(key)) return null
426
+ const initializer = resolver.expressionNode(definition.initializer)
427
+ if (!initializer) return null
428
+ state.dependencies.add(definition.id)
429
+ state.activeDefinitions.add(key)
430
+ try {
431
+ return evaluateDynamicNode(resolver, definition.id, initializer, state)
432
+ } finally {
433
+ state.activeDefinitions.delete(key)
434
+ }
435
+ }
436
+
437
+ if (node.type === 'MemberExpression') {
438
+ const objectNode = childNode(node, 'object')
439
+ const propertyNode = childNode(node, 'property')
440
+ if (!objectNode || !propertyNode) return null
441
+ const object = evaluateNode(resolver, id, objectNode, state)
442
+ if (!object.ok || !Array.isArray(object.value)) return null
443
+ const property = evaluateDynamicNode(resolver, id, propertyNode, state)
444
+ if (property?.type !== 'number' || object.value.length > 32) return null
445
+ const values: (number | string | boolean | null)[] = []
446
+ let type: DynamicEvaluation['type'] | null = null
447
+ for (const value of object.value) {
448
+ const nextType = dynamicPrimitive(value)
449
+ if (!nextType || (type && type !== nextType)) return null
450
+ type = nextType
451
+ const primitive = value as number | string | boolean | null
452
+ if (!values.includes(primitive)) values.push(primitive)
453
+ }
454
+ return type ? { type, values } : null
455
+ }
456
+
457
+ if (node.type === 'BinaryExpression') {
458
+ const leftNode = childNode(node, 'left')
459
+ const rightNode = childNode(node, 'right')
460
+ if (!leftNode || !rightNode) return null
461
+ const left = evaluateDynamicNode(resolver, id, leftNode, state)
462
+ const right = evaluateDynamicNode(resolver, id, rightNode, state)
463
+ if (node.operator === '+') {
464
+ return left?.type === 'number' && right?.type === 'number'
465
+ ? { type: 'number' }
466
+ : null
467
+ }
468
+ if (
469
+ node.operator === '-' ||
470
+ node.operator === '*' ||
471
+ node.operator === '/' ||
472
+ node.operator === '%' ||
473
+ node.operator === '**'
474
+ ) {
475
+ // successful javascript arithmetic with a number operand produces a
476
+ // number. bigint mixed with the numeric literals used here throws before
477
+ // the style value exists, so it does not create another runtime domain.
478
+ if (left?.type === 'number' || right?.type === 'number') {
479
+ return { type: 'number' }
480
+ }
481
+ }
482
+ }
483
+
484
+ // a template whose every interpolation takes finitely many primitives takes
485
+ // finitely many strings: `w-6 ${colors[index % 2]}` is two class strings
486
+ if (node.type === 'TemplateLiteral') {
487
+ const quasis = childNodes(node, 'quasis')
488
+ const expressions = childNodes(node, 'expressions')
489
+ let values: string[] = ['']
490
+ for (let position = 0; position < quasis.length; position++) {
491
+ const text = quasiText(quasis[position])
492
+ values = values.map((value) => value + text)
493
+ const expression = expressions[position]
494
+ if (!expression) continue
495
+ const part = evaluateDynamicNode(resolver, id, expression, state)
496
+ if (!part?.values) return null
497
+ const next: string[] = []
498
+ for (const value of values) {
499
+ for (const primitive of part.values) {
500
+ const joined = value + String(primitive)
501
+ if (!next.includes(joined)) next.push(joined)
502
+ }
503
+ }
504
+ if (next.length > 32) return null
505
+ values = next
506
+ }
507
+ return { type: 'string', values }
508
+ }
509
+
510
+ return null
511
+ }
512
+
513
+ export type BranchDecisionNode =
514
+ | {
515
+ kind: 'leaf'
516
+ value?: StaticEvaluationValue
517
+ dependencies: ResolvedModuleId[]
518
+ }
519
+ | {
520
+ kind: 'branch'
521
+ test: SourceSpan
522
+ whenTrue: BranchDecisionNode
523
+ whenFalse: BranchDecisionNode
524
+ }
525
+
526
+ export function collectLeaves(
527
+ node: BranchDecisionNode
528
+ ): { value?: StaticEvaluationValue; dependencies: ResolvedModuleId[] }[] {
529
+ if (node.kind === 'leaf') return [node]
530
+ return [...collectLeaves(node.whenTrue), ...collectLeaves(node.whenFalse)]
531
+ }
532
+
533
+ export function collectBranchDependencies(node: BranchDecisionNode): ResolvedModuleId[] {
534
+ if (node.kind === 'leaf') return node.dependencies
535
+ return [
536
+ ...new Set([
537
+ ...collectBranchDependencies(node.whenTrue),
538
+ ...collectBranchDependencies(node.whenFalse),
539
+ ]),
540
+ ].sort()
541
+ }
542
+
543
+ function evaluateBranchChild(
544
+ resolver: SymbolResolver,
545
+ id: ResolvedModuleId,
546
+ node: AstNode,
547
+ depth: number,
548
+ state: EvaluationState,
549
+ sourceId: ResolvedModuleId
550
+ ): BranchDecisionNode | null {
551
+ const leafState: EvaluationState = {
552
+ activeDefinitions: new Set(state.activeDefinitions),
553
+ dependencies: new Set(state.dependencies),
554
+ }
555
+ const leafRes = evaluateNode(resolver, id, node, leafState)
556
+ if (leafRes.ok) {
557
+ return {
558
+ kind: 'leaf',
559
+ value: leafRes.value,
560
+ dependencies: [...leafState.dependencies].sort(),
561
+ }
562
+ }
563
+ return evaluateBranchesNode(resolver, id, node, depth + 1, leafState, sourceId)
564
+ }
565
+
566
+ function evaluateBranchesNode(
567
+ resolver: SymbolResolver,
568
+ id: ResolvedModuleId,
569
+ input: AstNode,
570
+ depth: number,
571
+ state: EvaluationState,
572
+ sourceId: ResolvedModuleId
573
+ ): BranchDecisionNode | null {
574
+ const node = unwrapExpression(input)
575
+ if (!isAstNode(node)) return null
576
+
577
+ if (node.type === 'Identifier') {
578
+ if (!identifierName(node)) return null
579
+ const definition = resolver.resolveReference(expressionReference(id, node))
580
+ if (!definition || !definition.constant || !definition.initializer) return null
581
+ const initializer = resolver.expressionNode(definition.initializer)
582
+ if (!initializer) return null
583
+ const key = `${definition.id}:${definition.span.start}`
584
+ if (state.activeDefinitions.has(key)) return null
585
+ state.activeDefinitions.add(key)
586
+ state.dependencies.add(definition.id)
587
+ try {
588
+ return evaluateBranchesNode(
589
+ resolver,
590
+ definition.id,
591
+ initializer,
592
+ depth,
593
+ state,
594
+ sourceId
595
+ )
596
+ } finally {
597
+ state.activeDefinitions.delete(key)
598
+ }
599
+ }
600
+
601
+ if (depth >= 3) return null
602
+
603
+ if (node.type === 'ConditionalExpression') {
604
+ const testNode = childNode(node, 'test')
605
+ const consequent = childNode(node, 'consequent')
606
+ const alternate = childNode(node, 'alternate')
607
+ if (!testNode || !consequent || !alternate) return null
608
+
609
+ const test = spanOf(id, testNode)
610
+ if (test.id !== sourceId) return null
611
+ const whenTrue = evaluateBranchChild(resolver, id, consequent, depth, state, sourceId)
612
+ if (!whenTrue) return null
613
+ const whenFalse = evaluateBranchChild(resolver, id, alternate, depth, state, sourceId)
614
+ if (!whenFalse) return null
615
+
616
+ return {
617
+ kind: 'branch',
618
+ test,
619
+ whenTrue,
620
+ whenFalse,
621
+ }
622
+ }
623
+
624
+ if (node.type === 'TemplateLiteral') {
625
+ return templateBranchesNode(
626
+ resolver,
627
+ id,
628
+ childNodes(node, 'quasis'),
629
+ childNodes(node, 'expressions'),
630
+ 0,
631
+ '',
632
+ depth,
633
+ state,
634
+ sourceId
635
+ )
636
+ }
637
+
638
+ if (node.type === 'LogicalExpression') {
639
+ const left = childNode(node, 'left')
640
+ const right = childNode(node, 'right')
641
+ if (!left || !right) return null
642
+
643
+ if (node.operator === '&&') {
644
+ const test = spanOf(id, left)
645
+ if (test.id !== sourceId) return null
646
+ const whenTrue = evaluateBranchChild(resolver, id, right, depth, state, sourceId)
647
+ if (!whenTrue) return null
648
+ const whenFalse: BranchDecisionNode = {
649
+ kind: 'leaf',
650
+ value: undefined,
651
+ dependencies: [...state.dependencies].sort(),
652
+ }
653
+ return {
654
+ kind: 'branch',
655
+ test,
656
+ whenTrue,
657
+ whenFalse,
658
+ }
659
+ }
660
+
661
+ if (node.operator === '||' || node.operator === '??') {
662
+ const test = spanOf(id, left)
663
+ if (test.id !== sourceId) return null
664
+ const leftState: EvaluationState = {
665
+ activeDefinitions: new Set(state.activeDefinitions),
666
+ dependencies: new Set(state.dependencies),
667
+ }
668
+ const leftRes = evaluateNode(resolver, id, left, leftState)
669
+ if (!leftRes.ok) return null
670
+ const whenTrue: BranchDecisionNode = {
671
+ kind: 'leaf',
672
+ value: leftRes.value,
673
+ dependencies: [...leftState.dependencies].sort(),
674
+ }
675
+ const whenFalse = evaluateBranchChild(resolver, id, right, depth, state, sourceId)
676
+ if (!whenFalse) return null
677
+ return {
678
+ kind: 'branch',
679
+ test,
680
+ whenTrue,
681
+ whenFalse,
682
+ }
683
+ }
684
+ }
685
+
686
+ return null
687
+ }
688
+
689
+ function mapBranchLeaves(
690
+ node: BranchDecisionNode,
691
+ map: (leaf: Extract<BranchDecisionNode, { kind: 'leaf' }>) => BranchDecisionNode | null
692
+ ): BranchDecisionNode | null {
693
+ if (node.kind === 'leaf') return map(node)
694
+ const whenTrue = mapBranchLeaves(node.whenTrue, map)
695
+ if (!whenTrue) return null
696
+ const whenFalse = mapBranchLeaves(node.whenFalse, map)
697
+ if (!whenFalse) return null
698
+ return { kind: 'branch', test: node.test, whenTrue, whenFalse }
699
+ }
700
+
701
+ /**
702
+ * A template literal is a decision tree once every value it interpolates is:
703
+ * `${active ? 'red10' : 'blue10'}` names the same two strings the bare ternary
704
+ * does, and the quasis around it are constants. Each interpolated tree
705
+ * multiplies the leaves, so every step spends a level of the shared depth
706
+ * budget and a wide template refuses instead of expanding.
707
+ */
708
+ function templateBranchesNode(
709
+ resolver: SymbolResolver,
710
+ id: ResolvedModuleId,
711
+ quasis: AstNode[],
712
+ expressions: AstNode[],
713
+ index: number,
714
+ prefix: string,
715
+ depth: number,
716
+ state: EvaluationState,
717
+ sourceId: ResolvedModuleId
718
+ ): BranchDecisionNode | null {
719
+ let text = prefix
720
+ for (let position = index; position < quasis.length; position++) {
721
+ text += quasiText(quasis[position])
722
+ const expression = expressions[position]
723
+ if (!expression) continue
724
+ const exact = evaluateNode(resolver, id, expression, state)
725
+ if (exact.ok) {
726
+ const interpolated = templateInterpolation(exact.value)
727
+ if (interpolated === null) return null
728
+ text += interpolated
729
+ continue
730
+ }
731
+ const tree = evaluateBranchesNode(
732
+ resolver,
733
+ id,
734
+ expression,
735
+ depth + 1,
736
+ state,
737
+ sourceId
738
+ )
739
+ if (!tree) return null
740
+ return mapBranchLeaves(tree, (leaf) => {
741
+ // an `&&` leaf carries `undefined` to mean "the falsy left operand", whose
742
+ // string form is the operand itself and so is not known here
743
+ if (leaf.value === undefined) return null
744
+ const interpolated = templateInterpolation(leaf.value)
745
+ if (interpolated === null) return null
746
+ return templateBranchesNode(
747
+ resolver,
748
+ id,
749
+ quasis,
750
+ expressions,
751
+ position + 1,
752
+ text + interpolated,
753
+ depth + 1,
754
+ state,
755
+ sourceId
756
+ )
757
+ })
758
+ }
759
+ return { kind: 'leaf', value: text, dependencies: [...state.dependencies].sort() }
760
+ }
761
+
762
+ /** ToString on a primitive, which is all a template can be folded through */
763
+ function templateInterpolation(value: StaticEvaluationValue): string | null {
764
+ if (value === null) return 'null'
765
+ if (
766
+ typeof value === 'string' ||
767
+ typeof value === 'number' ||
768
+ typeof value === 'boolean'
769
+ ) {
770
+ return String(value)
771
+ }
772
+ return null
773
+ }
774
+
775
+ /**
776
+ * A conditional or logical expression whose test resists static evaluation while
777
+ * its branch values evaluate statically or form a decision tree.
778
+ */
779
+ export function evaluateBranches(
780
+ resolver: SymbolResolver,
781
+ reference: ExpressionReference
782
+ ): BranchDecisionNode | null {
783
+ const node = resolver.expressionNode(reference)
784
+ if (!node) return null
785
+ const state: EvaluationState = {
786
+ activeDefinitions: new Set(),
787
+ dependencies: new Set(),
788
+ }
789
+ const tree = evaluateBranchesNode(resolver, reference.id, node, 0, state, reference.id)
790
+ return tree?.kind === 'branch' ? tree : null
791
+ }
792
+
793
+ export function evaluateExpression(
794
+ resolver: SymbolResolver,
795
+ reference: ExpressionReference
796
+ ): EvaluationResult {
797
+ const node = resolver.expressionNode(reference)
798
+ if (!node) {
799
+ return {
800
+ ok: false,
801
+ bailout: linkedBailout(
802
+ 'linked/missing-initializer',
803
+ reference,
804
+ 'Expression is outside the linked project graph',
805
+ reference.id
806
+ ),
807
+ }
808
+ }
809
+ const state: EvaluationState = {
810
+ activeDefinitions: new Set(),
811
+ dependencies: new Set(),
812
+ }
813
+ const result = evaluateNode(resolver, reference.id, node, state)
814
+ return result.ok
815
+ ? {
816
+ ok: true,
817
+ value: result.value,
818
+ dependencies: [...state.dependencies].sort(),
819
+ }
820
+ : result
821
+ }
822
+
823
+ /**
824
+ * proves the runtime domain of a dynamic expression without evaluating its
825
+ * changing inputs. this is deliberately narrow: numeric arithmetic and a
826
+ * bounded lookup into a static primitive array are the two forms a host style
827
+ * can consume directly without retaining the full Tamagui resolver.
828
+ */
829
+ export function evaluateDynamicExpression(
830
+ resolver: SymbolResolver,
831
+ reference: ExpressionReference
832
+ ): DynamicEvaluation | null {
833
+ const node = resolver.expressionNode(reference)
834
+ if (!node) return null
835
+ const state: EvaluationState = {
836
+ activeDefinitions: new Set(),
837
+ dependencies: new Set(),
838
+ }
839
+ const result = evaluateDynamicNode(resolver, reference.id, node, state)
840
+ return result ? { ...result, dependencies: [...state.dependencies].sort() } : null
841
+ }
842
+
843
+ export function evaluateBinding(
844
+ resolver: SymbolResolver,
845
+ id: ResolvedModuleId,
846
+ localName: string
847
+ ): EvaluationResult {
848
+ const definition = resolver.resolveBinding(id, localName)
849
+ if (!definition) {
850
+ const emptySpan: SourceSpan = { id, start: 0, end: 0 }
851
+ return {
852
+ ok: false,
853
+ bailout: linkedBailout(
854
+ 'linked/unresolved-binding',
855
+ emptySpan,
856
+ `Binding ${localName} has no host-linked definition`
857
+ ),
858
+ }
859
+ }
860
+ if (!definition.initializer) {
861
+ return {
862
+ ok: false,
863
+ bailout: linkedBailout(
864
+ 'linked/missing-initializer',
865
+ definition.span,
866
+ `Binding ${localName} has no static initializer`,
867
+ definition.id
868
+ ),
869
+ }
870
+ }
871
+ return evaluateExpression(
872
+ resolver,
873
+ expressionReference(definition.id, definition.initializer)
874
+ )
875
+ }