1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import prettyFormat from 'pretty-format';
9
+import {CompilerDiagnostic, CompilerError, SourceLocation} from '..';
10
+import {ErrorCategory} from '../CompilerError';
11
+import {
12
+ areEqualPaths,
13
+ BlockId,
14
+ DependencyPath,
15
+ FinishMemoize,
16
+ HIRFunction,
17
+ Identifier,
18
+ IdentifierId,
19
+ InstructionKind,
20
+ isSubPath,
21
+ LoadGlobal,
22
+ ManualMemoDependency,
23
+ Place,
24
+ StartMemoize,
25
+} from '../HIR';
26
+import {
27
+ eachInstructionLValue,
28
+ eachInstructionValueLValue,
29
+ eachInstructionValueOperand,
30
+ eachTerminalOperand,
31
+} from '../HIR/visitors';
32
+import {Result} from '../Utils/Result';
33
+import {retainWhere} from '../Utils/utils';
34
+
35
+const DEBUG = false;
36
+
37
+/**
38
+ * Validates that existing manual memoization had exhaustive dependencies.
39
+ * Memoization with missing or extra reactive dependencies is invalid React
40
+ * and compilation can change behavior, causing a value to be computed more
41
+ * or less times.
42
+ *
43
+ * TODOs:
44
+ * - Better handling of cases where we infer multiple dependencies related to a single
45
+ * variable. Eg if the user has dep `x` and we inferred `x.y, x.z`, the user's dep
46
+ * is sufficient.
47
+ * - Handle cases where the user deps were not simple identifiers + property chains.
48
+ * We try to detect this in ValidateUseMemo but we miss some cases. The problem
49
+ * is that invalid forms can be value blocks or function calls that don't get
50
+ * removed by DCE, leaving a structure like:
51
+ *
52
+ * StartMemoize
53
+ * t0 = <value to memoize>
54
+ * ...non-DCE'd code for manual deps...
55
+ * FinishMemoize decl=t0
56
+ *
57
+ * When we go to compute the dependencies, we then think that the user's manual dep
58
+ * logic is part of what the memo computation logic.
59
+ */
60
+export function validateExhaustiveDependencies(
61
+ fn: HIRFunction,
62
+): Result<void, CompilerError> {
63
+ const reactive = collectReactiveIdentifiersHIR(fn);
64
+
65
+ const temporaries: Map<IdentifierId, Temporary> = new Map();
66
+ for (const param of fn.params) {
67
+ const place = param.kind === 'Identifier' ? param : param.place;
68
+ temporaries.set(place.identifier.id, {
69
+ kind: 'Local',
70
+ identifier: place.identifier,
71
+ path: [],
72
+ context: false,
73
+ loc: place.loc,
74
+ });
75
+ }
76
+ const error = new CompilerError();
77
+ let startMemo: StartMemoize | null = null;
78
+
79
+ function onStartMemoize(
80
+ value: StartMemoize,
81
+ dependencies: Set<InferredDependency>,
82
+ locals: Set<IdentifierId>,
83
+ ): void {
84
+ CompilerError.simpleInvariant(startMemo == null, {
85
+ reason: 'Unexpected nested memo calls',
86
+ loc: value.loc,
87
+ });
88
+ startMemo = value;
89
+ dependencies.clear();
90
+ locals.clear();
91
+ }
92
+ function onFinishMemoize(
93
+ value: FinishMemoize,
94
+ dependencies: Set<InferredDependency>,
95
+ locals: Set<IdentifierId>,
96
+ ): void {
97
+ CompilerError.simpleInvariant(
98
+ startMemo != null && startMemo.manualMemoId === value.manualMemoId,
99
+ {
100
+ reason: 'Found FinishMemoize without corresponding StartMemoize',
101
+ loc: value.loc,
102
+ },
103
+ );
104
+ visitCandidateDependency(value.decl, temporaries, dependencies, locals);
105
+ const inferred: Array<InferredDependency> = Array.from(dependencies);
106
+ // Sort dependencies by name, and path, with shorter/non-optional paths first
107
+ inferred.sort((a, b) => {
108
+ if (a.kind === 'Global' && b.kind == 'Global') {
109
+ return a.binding.name.localeCompare(b.binding.name);
110
+ } else if (a.kind == 'Local' && b.kind == 'Local') {
111
+ CompilerError.simpleInvariant(
112
+ a.identifier.name != null &&
113
+ a.identifier.name.kind === 'named' &&
114
+ b.identifier.name != null &&
115
+ b.identifier.name.kind === 'named',
116
+ {
117
+ reason: 'Expected dependencies to be named variables',
118
+ loc: a.loc,
119
+ },
120
+ );
121
+ if (a.identifier.id !== b.identifier.id) {
122
+ return a.identifier.name.value.localeCompare(b.identifier.name.value);
123
+ }
124
+ if (a.path.length !== b.path.length) {
125
+ // if a's path is shorter this returns a negative, sorting a first
126
+ return a.path.length - b.path.length;
127
+ }
128
+ for (let i = 0; i < a.path.length; i++) {
129
+ const aProperty = a.path[i];
130
+ const bProperty = b.path[i];
131
+ const aOptional = aProperty.optional ? 0 : 1;
132
+ const bOptional = bProperty.optional ? 0 : 1;
133
+ if (aOptional !== bOptional) {
134
+ // sort non-optionals first
135
+ return aOptional - bOptional;
136
+ } else if (aProperty.property !== bProperty.property) {
137
+ return String(aProperty.property).localeCompare(
138
+ String(bProperty.property),
139
+ );
140
+ }
141
+ }
142
+ return 0;
143
+ } else {
144
+ const aName =
145
+ a.kind === 'Global' ? a.binding.name : a.identifier.name?.value;
146
+ const bName =
147
+ b.kind === 'Global' ? b.binding.name : b.identifier.name?.value;
148
+ if (aName != null && bName != null) {
149
+ return aName.localeCompare(bName);
150
+ }
151
+ return 0;
152
+ }
153
+ });
154
+ // remove redundant inferred dependencies
155
+ retainWhere(inferred, (dep, ix) => {
156
+ const match = inferred.findIndex(prevDep => {
157
+ return (
158
+ isEqualTemporary(prevDep, dep) ||
159
+ (prevDep.kind === 'Local' &&
160
+ dep.kind === 'Local' &&
161
+ prevDep.identifier.id === dep.identifier.id &&
162
+ isSubPath(prevDep.path, dep.path))
163
+ );
164
+ });
165
+ // only retain entries that don't have a prior match
166
+ return match === -1 || match >= ix;
167
+ });
168
+ // Validate that all manual dependencies belong there
169
+ if (DEBUG) {
170
+ console.log('manual');
171
+ console.log(
172
+ (startMemo.deps ?? [])
173
+ .map(x => ' ' + printManualMemoDependency(x))
174
+ .join('\n'),
175
+ );
176
+ console.log('inferred');
177
+ console.log(
178
+ inferred.map(x => ' ' + printInferredDependency(x)).join('\n'),
179
+ );
180
+ }
181
+ const manualDependencies = startMemo.deps ?? [];
182
+ const matched: Set<ManualMemoDependency> = new Set();
183
+ const missing: Array<Extract<InferredDependency, {kind: 'Local'}>> = [];
184
+ const extra: Array<ManualMemoDependency> = [];
185
+ for (const inferredDependency of inferred) {
186
+ if (inferredDependency.kind === 'Global') {
187
+ for (const manualDependency of manualDependencies) {
188
+ if (
189
+ manualDependency.root.kind === 'Global' &&
190
+ manualDependency.root.identifierName ===
191
+ inferredDependency.binding.name
192
+ ) {
193
+ matched.add(manualDependency);
194
+ extra.push(manualDependency);
195
+ }
196
+ }
197
+ continue;
198
+ }
199
+ CompilerError.simpleInvariant(inferredDependency.kind === 'Local', {
200
+ reason: 'Unexpected function dependency',
201
+ loc: value.loc,
202
+ });
203
+ let hasMatchingManualDependency = false;
204
+ for (const manualDependency of manualDependencies) {
205
+ if (
206
+ manualDependency.root.kind === 'NamedLocal' &&
207
+ manualDependency.root.value.identifier.id ===
208
+ inferredDependency.identifier.id &&
209
+ (areEqualPaths(manualDependency.path, inferredDependency.path) ||
210
+ isSubPath(manualDependency.path, inferredDependency.path))
211
+ ) {
212
+ hasMatchingManualDependency = true;
213
+ matched.add(manualDependency);
214
+ }
215
+ }
216
+ if (!hasMatchingManualDependency) {
217
+ missing.push(inferredDependency);
218
+ }
219
+ }
220
+
221
+ for (const dep of startMemo.deps ?? []) {
222
+ if (
223
+ matched.has(dep) ||
224
+ (dep.root.kind === 'NamedLocal' &&
225
+ !reactive.has(dep.root.value.identifier.id))
226
+ ) {
227
+ continue;
228
+ }
229
+ extra.push(dep);
230
+ }
231
+
232
+ if (missing.length !== 0) {
233
+ // Error
234
+ const diagnostic = CompilerDiagnostic.create({
235
+ category: ErrorCategory.PreserveManualMemo,
236
+ reason: 'Found non-exhaustive dependencies',
237
+ description:
238
+ 'Missing dependencies can cause a value not to update when those inputs change, ' +
239
+ 'resulting in stale UI. This memoization cannot be safely rewritten by the compiler.',
240
+ });
241
+ for (const dep of missing) {
242
+ diagnostic.withDetails({
243
+ kind: 'error',
244
+ message: `Missing dependency \`${printInferredDependency(dep)}\``,
245
+ loc: dep.loc,
246
+ });
247
+ }
248
+ error.pushDiagnostic(diagnostic);
249
+ } else if (extra.length !== 0) {
250
+ const diagnostic = CompilerDiagnostic.create({
251
+ category: ErrorCategory.PreserveManualMemo,
252
+ reason: 'Found unnecessary memoization dependencies',
253
+ description:
254
+ 'Unnecessary dependencies can cause a value to update more often than necessary, ' +
255
+ 'which can cause effects to run more than expected. This memoization cannot be safely ' +
256
+ 'rewritten by the compiler',
257
+ });
258
+ diagnostic.withDetails({
259
+ kind: 'error',
260
+ message: `Unnecessary dependencies ${extra.map(dep => `\`${printManualMemoDependency(dep)}\``).join(', ')}`,
261
+ loc: value.loc,
262
+ });
263
+ error.pushDiagnostic(diagnostic);
264
+ }
265
+
266
+ dependencies.clear();
267
+ locals.clear();
268
+ startMemo = null;
269
+ }
270
+
271
+ collectDependencies(fn, temporaries, {
272
+ onStartMemoize,
273
+ onFinishMemoize,
274
+ });
275
+ return error.asResult();
276
+}
277
+
278
+function addDependency(
279
+ dep: Temporary,
280
+ dependencies: Set<InferredDependency>,
281
+ locals: Set<IdentifierId>,
282
+): void {
283
+ if (dep.kind === 'Function') {
284
+ for (const x of dep.dependencies) {
285
+ addDependency(x, dependencies, locals);
286
+ }
287
+ } else if (dep.kind === 'Global') {
288
+ dependencies.add(dep);
289
+ } else if (!locals.has(dep.identifier.id)) {
290
+ dependencies.add(dep);
291
+ }
292
+}
293
+
294
+function visitCandidateDependency(
295
+ place: Place,
296
+ temporaries: Map<IdentifierId, Temporary>,
297
+ dependencies: Set<InferredDependency>,
298
+ locals: Set<IdentifierId>,
299
+): void {
300
+ const dep = temporaries.get(place.identifier.id);
301
+ if (dep != null) {
302
+ addDependency(dep, dependencies, locals);
303
+ }
304
+}
305
+
306
+/**
307
+ * This function determines the dependencies of the given function relative to
308
+ * its external context. Dependencies are collected eagerly, the first time an
309
+ * external variable is referenced, as opposed to trying to delay or aggregate
310
+ * calculation of dependencies until they are later "used".
311
+ *
312
+ * For example, in
313
+ *
314
+ * ```
315
+ * function f() {
316
+ * let x = y; // we record a dependency on `y` here
317
+ * ...
318
+ * use(x); // as opposed to trying to delay that dependency until here
319
+ * }
320
+ * ```
321
+ *
322
+ * That said, LoadLocal/LoadContext does not immediately take a dependency,
323
+ * we store the dependency in a temporary and set it as used when that temporary
324
+ * is referenced as an operand.
325
+ *
326
+ * As we proceed through the function we track local variables that it creates
327
+ * and don't consider later references to these variables as dependencies.
328
+ *
329
+ * For function expressions we first collect the function's dependencies by
330
+ * calling this function recursively, _without_ taking into account whether
331
+ * the "external" variables it accesses are actually external or just locals
332
+ * in the parent. We then prune any locals and immediately consider any
333
+ * remaining externals that it accesses as a dependency:
334
+ *
335
+ * ```
336
+ * function Component() {
337
+ * const local = ...;
338
+ * const f = () => { return [external, local] };
339
+ * }
340
+ * ```
341
+ *
342
+ * Here we calculate `f` as having dependencies `external, `local` and save
343
+ * this into `temporaries`. We then also immediately take these as dependencies
344
+ * at the Component scope, at which point we filter out `local` as a local variable,
345
+ * leaving just a dependency on `external`.
346
+ *
347
+ * When calling this function on a top-level component or hook, the collected dependencies
348
+ * will only contain the globals that it accesses which isn't useful. Instead, passing
349
+ * onStartMemoize/onFinishMemoize callbacks allows looking at the dependencies within
350
+ * blocks of manual memoization.
351
+ */
352
+function collectDependencies(
353
+ fn: HIRFunction,
354
+ temporaries: Map<IdentifierId, Temporary>,
355
+ callbacks: {
356
+ onStartMemoize: (
357
+ startMemo: StartMemoize,
358
+ dependencies: Set<InferredDependency>,
359
+ locals: Set<IdentifierId>,
360
+ ) => void;
361
+ onFinishMemoize: (
362
+ finishMemo: FinishMemoize,
363
+ dependencies: Set<InferredDependency>,
364
+ locals: Set<IdentifierId>,
365
+ ) => void;
366
+ } | null,
367
+): Extract<Temporary, {kind: 'Function'}> {
368
+ const optionals = findOptionalPlaces(fn);
369
+ if (DEBUG) {
370
+ console.log(prettyFormat(optionals));
371
+ }
372
+ const locals: Set<IdentifierId> = new Set();
373
+ const dependencies: Set<InferredDependency> = new Set();
374
+ function visit(place: Place): void {
375
+ visitCandidateDependency(place, temporaries, dependencies, locals);
376
+ }
377
+ for (const block of fn.body.blocks.values()) {
378
+ for (const phi of block.phis) {
379
+ let deps: Array<Temporary> | null = null;
380
+ for (const operand of phi.operands.values()) {
381
+ const dep = temporaries.get(operand.identifier.id);
382
+ if (dep == null) {
383
+ continue;
384
+ }
385
+ if (deps == null) {
386
+ deps = [dep];
387
+ } else {
388
+ deps.push(dep);
389
+ }
390
+ }
391
+ if (deps == null) {
392
+ continue;
393
+ } else if (deps.length === 1) {
394
+ temporaries.set(phi.place.identifier.id, deps[0]!);
395
+ } else {
396
+ temporaries.set(phi.place.identifier.id, {
397
+ kind: 'Function',
398
+ dependencies: new Set(deps),
399
+ });
400
+ }
401
+ }
402
+
403
+ for (const instr of block.instructions) {
404
+ const {lvalue, value} = instr;
405
+ switch (value.kind) {
406
+ case 'LoadGlobal': {
407
+ temporaries.set(lvalue.identifier.id, {
408
+ kind: 'Global',
409
+ binding: value.binding,
410
+ });
411
+ break;
412
+ }
413
+ case 'LoadContext':
414
+ case 'LoadLocal': {
415
+ if (locals.has(value.place.identifier.id)) {
416
+ break;
417
+ }
418
+ const temp = temporaries.get(value.place.identifier.id);
419
+ if (temp != null) {
420
+ if (temp.kind === 'Local') {
421
+ const local: Temporary = {...temp, loc: value.place.loc};
422
+ temporaries.set(lvalue.identifier.id, local);
423
+ } else {
424
+ temporaries.set(lvalue.identifier.id, temp);
425
+ }
426
+ }
427
+ break;
428
+ }
429
+ case 'DeclareLocal': {
430
+ const local: Temporary = {
431
+ kind: 'Local',
432
+ identifier: value.lvalue.place.identifier,
433
+ path: [],
434
+ context: false,
435
+ loc: value.lvalue.place.loc,
436
+ };
437
+ temporaries.set(value.lvalue.place.identifier.id, local);
438
+ locals.add(value.lvalue.place.identifier.id);
439
+ break;
440
+ }
441
+ case 'StoreLocal': {
442
+ if (value.lvalue.place.identifier.name == null) {
443
+ const temp = temporaries.get(value.value.identifier.id);
444
+ if (temp != null) {
445
+ temporaries.set(value.lvalue.place.identifier.id, temp);
446
+ }
447
+ break;
448
+ }
449
+ visit(value.value);
450
+ if (value.lvalue.kind !== InstructionKind.Reassign) {
451
+ const local: Temporary = {
452
+ kind: 'Local',
453
+ identifier: value.lvalue.place.identifier,
454
+ path: [],
455
+ context: false,
456
+ loc: value.lvalue.place.loc,
457
+ };
458
+ temporaries.set(value.lvalue.place.identifier.id, local);
459
+ locals.add(value.lvalue.place.identifier.id);
460
+ }
461
+ break;
462
+ }
463
+ case 'DeclareContext': {
464
+ const local: Temporary = {
465
+ kind: 'Local',
466
+ identifier: value.lvalue.place.identifier,
467
+ path: [],
468
+ context: true,
469
+ loc: value.lvalue.place.loc,
470
+ };
471
+ temporaries.set(value.lvalue.place.identifier.id, local);
472
+ break;
473
+ }
474
+ case 'StoreContext': {
475
+ visit(value.value);
476
+ if (value.lvalue.kind !== InstructionKind.Reassign) {
477
+ const local: Temporary = {
478
+ kind: 'Local',
479
+ identifier: value.lvalue.place.identifier,
480
+ path: [],
481
+ context: true,
482
+ loc: value.lvalue.place.loc,
483
+ };
484
+ temporaries.set(value.lvalue.place.identifier.id, local);
485
+ locals.add(value.lvalue.place.identifier.id);
486
+ }
487
+ break;
488
+ }
489
+ case 'Destructure': {
490
+ visit(value.value);
491
+ if (value.lvalue.kind !== InstructionKind.Reassign) {
492
+ for (const lvalue of eachInstructionValueLValue(value)) {
493
+ const local: Temporary = {
494
+ kind: 'Local',
495
+ identifier: lvalue.identifier,
496
+ path: [],
497
+ context: false,
498
+ loc: lvalue.loc,
499
+ };
500
+ temporaries.set(lvalue.identifier.id, local);
501
+ locals.add(lvalue.identifier.id);
502
+ }
503
+ }
504
+ break;
505
+ }
506
+ case 'PropertyLoad': {
507
+ if (typeof value.property === 'number') {
508
+ visit(value.object);
509
+ break;
510
+ }
511
+ const object = temporaries.get(value.object.identifier.id);
512
+ if (object != null && object.kind === 'Local') {
513
+ const optional = optionals.get(value.object.identifier.id) ?? false;
514
+ const local: Temporary = {
515
+ kind: 'Local',
516
+ identifier: object.identifier,
517
+ context: object.context,
518
+ path: [
519
+ ...object.path,
520
+ {
521
+ optional,
522
+ property: value.property,
523
+ },
524
+ ],
525
+ loc: value.loc,
526
+ };
527
+ temporaries.set(lvalue.identifier.id, local);
528
+ }
529
+ break;
530
+ }
531
+ case 'FunctionExpression':
532
+ case 'ObjectMethod': {
533
+ const functionDeps = collectDependencies(
534
+ value.loweredFunc.func,
535
+ temporaries,
536
+ null,
537
+ );
538
+ temporaries.set(lvalue.identifier.id, functionDeps);
539
+ addDependency(functionDeps, dependencies, locals);
540
+ break;
541
+ }
542
+ case 'StartMemoize': {
543
+ const onStartMemoize = callbacks?.onStartMemoize;
544
+ if (onStartMemoize != null) {
545
+ onStartMemoize(value, dependencies, locals);
546
+ }
547
+ break;
548
+ }
549
+ case 'FinishMemoize': {
550
+ const onFinishMemoize = callbacks?.onFinishMemoize;
551
+ if (onFinishMemoize != null) {
552
+ onFinishMemoize(value, dependencies, locals);
553
+ }
554
+ break;
555
+ }
556
+ case 'MethodCall': {
557
+ // Ignore the method itself
558
+ for (const operand of eachInstructionValueOperand(value)) {
559
+ if (operand.identifier.id === value.property.identifier.id) {
560
+ continue;
561
+ }
562
+ visit(operand);
563
+ }
564
+ break;
565
+ }
566
+ default: {
567
+ for (const operand of eachInstructionValueOperand(value)) {
568
+ visit(operand);
569
+ }
570
+ for (const lvalue of eachInstructionLValue(instr)) {
571
+ locals.add(lvalue.identifier.id);
572
+ }
573
+ }
574
+ }
575
+ }
576
+ for (const operand of eachTerminalOperand(block.terminal)) {
577
+ if (optionals.has(operand.identifier.id)) {
578
+ continue;
579
+ }
580
+ visit(operand);
581
+ }
582
+ }
583
+ return {kind: 'Function', dependencies};
584
+}
585
+
586
+function printInferredDependency(dep: InferredDependency): string {
587
+ switch (dep.kind) {
588
+ case 'Global': {
589
+ return dep.binding.name;
590
+ }
591
+ case 'Local': {
592
+ CompilerError.simpleInvariant(
593
+ dep.identifier.name != null && dep.identifier.name.kind === 'named',
594
+ {
595
+ reason: 'Expected dependencies to be named variables',
596
+ loc: dep.loc,
597
+ },
598
+ );
599
+ return `${dep.identifier.name.value}${dep.path.map(p => (p.optional ? '?' : '') + '.' + p.property).join('')}`;
600
+ }
601
+ }
602
+}
603
+
604
+function printManualMemoDependency(dep: ManualMemoDependency): string {
605
+ let identifierName: string;
606
+ if (dep.root.kind === 'Global') {
607
+ identifierName = dep.root.identifierName;
608
+ } else {
609
+ const name = dep.root.value.identifier.name;
610
+ CompilerError.simpleInvariant(name != null && name.kind === 'named', {
611
+ reason: 'Expected manual dependencies to be named variables',
612
+ loc: dep.root.value.loc,
613
+ });
614
+ identifierName = name.value;
615
+ }
616
+ return `${identifierName}${dep.path.map(p => (p.optional ? '?' : '') + '.' + p.property).join('')}`;
617
+}
618
+
619
+function isEqualTemporary(a: Temporary, b: Temporary): boolean {
620
+ switch (a.kind) {
621
+ case 'Function': {
622
+ return false;
623
+ }
624
+ case 'Global': {
625
+ return b.kind === 'Global' && a.binding.name === b.binding.name;
626
+ }
627
+ case 'Local': {
628
+ return (
629
+ b.kind === 'Local' &&
630
+ a.identifier.id === b.identifier.id &&
631
+ areEqualPaths(a.path, b.path)
632
+ );
633
+ }
634
+ }
635
+}
636
+
637
+type Temporary =
638
+ | {kind: 'Global'; binding: LoadGlobal['binding']}
639
+ | {
640
+ kind: 'Local';
641
+ identifier: Identifier;
642
+ path: DependencyPath;
643
+ context: boolean;
644
+ loc: SourceLocation;
645
+ }
646
+ | {kind: 'Function'; dependencies: Set<Temporary>};
647
+type InferredDependency = Extract<Temporary, {kind: 'Local' | 'Global'}>;
648
+
649
+function collectReactiveIdentifiersHIR(fn: HIRFunction): Set<IdentifierId> {
650
+ const reactive = new Set<IdentifierId>();
651
+ for (const block of fn.body.blocks.values()) {
652
+ for (const instr of block.instructions) {
653
+ for (const lvalue of eachInstructionLValue(instr)) {
654
+ if (lvalue.reactive) {
655
+ reactive.add(lvalue.identifier.id);
656
+ }
657
+ }
658
+ for (const operand of eachInstructionValueOperand(instr.value)) {
659
+ if (operand.reactive) {
660
+ reactive.add(operand.identifier.id);
661
+ }
662
+ }
663
+ }
664
+ for (const operand of eachTerminalOperand(block.terminal)) {
665
+ if (operand.reactive) {
666
+ reactive.add(operand.identifier.id);
667
+ }
668
+ }
669
+ }
670
+ return reactive;
671
+}
672
+
673
+export function findOptionalPlaces(
674
+ fn: HIRFunction,
675
+): Map<IdentifierId, boolean> {
676
+ const optionals = new Map<IdentifierId, boolean>();
677
+ const visited: Set<BlockId> = new Set();
678
+ for (const [, block] of fn.body.blocks) {
679
+ if (visited.has(block.id)) {
680
+ continue;
681
+ }
682
+ if (block.terminal.kind === 'optional') {
683
+ visited.add(block.id);
684
+ const optionalTerminal = block.terminal;
685
+ let testBlock = fn.body.blocks.get(block.terminal.test)!;
686
+ const queue: Array<boolean | null> = [block.terminal.optional];
687
+ loop: while (true) {
688
+ visited.add(testBlock.id);
689
+ const terminal = testBlock.terminal;
690
+ switch (terminal.kind) {
691
+ case 'branch': {
692
+ const isOptional = queue.pop();
693
+ CompilerError.simpleInvariant(isOptional !== undefined, {
694
+ reason:
695
+ 'Expected an optional value for each optional test condition',
696
+ loc: terminal.test.loc,
697
+ });
698
+ if (isOptional != null) {
699
+ optionals.set(terminal.test.identifier.id, isOptional);
700
+ }
701
+ if (terminal.fallthrough === optionalTerminal.fallthrough) {
702
+ // found it
703
+ const consequent = fn.body.blocks.get(terminal.consequent)!;
704
+ const last = consequent.instructions.at(-1);
705
+ if (last !== undefined && last.value.kind === 'StoreLocal') {
706
+ if (isOptional != null) {
707
+ optionals.set(last.value.value.identifier.id, isOptional);
708
+ }
709
+ }
710
+ break loop;
711
+ } else {
712
+ testBlock = fn.body.blocks.get(terminal.fallthrough)!;
713
+ }
714
+ break;
715
+ }
716
+ case 'optional': {
717
+ queue.push(terminal.optional);
718
+ testBlock = fn.body.blocks.get(terminal.test)!;
719
+ break;
720
+ }
721
+ case 'logical':
722
+ case 'ternary': {
723
+ queue.push(null);
724
+ testBlock = fn.body.blocks.get(terminal.test)!;
725
+ break;
726
+ }
727
+
728
+ case 'sequence': {
729
+ // Do we need sequence?? In any case, don't push to queue bc there is no corresponding branch terminal
730
+ testBlock = fn.body.blocks.get(terminal.block)!;
731
+ break;
732
+ }
733
+ default: {
734
+ CompilerError.simpleInvariant(false, {
735
+ reason: `Unexpected terminal in optional`,
736
+ loc: terminal.loc,
737
+ });
738
+ }
739
+ }
740
+ }
741
+ CompilerError.simpleInvariant(queue.length === 0, {
742
+ reason:
743
+ 'Expected a matching number of conditional blocks and branch points',
744
+ loc: block.terminal.loc,
745
+ });
746
+ }
747
+ }
748
+ return optionals;
749
+}