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 {CompilerError} from '../CompilerError';
9
-import {Environment} from '../HIR';
10
-import {
11
- areEqualPaths,
12
- BlockId,
13
- DeclarationId,
14
- GeneratedSource,
15
- Identifier,
16
- InstructionId,
17
- InstructionKind,
18
- isObjectMethodType,
19
- isRefValueType,
20
- isUseRefType,
21
- makeInstructionId,
22
- Place,
23
- PrunedReactiveScopeBlock,
24
- ReactiveFunction,
25
- ReactiveInstruction,
26
- ReactiveOptionalCallValue,
27
- ReactiveScope,
28
- ReactiveScopeBlock,
29
- ReactiveScopeDependency,
30
- ReactiveTerminalStatement,
31
- ReactiveValue,
32
- ScopeId,
33
-} from '../HIR/HIR';
34
-import {eachInstructionValueOperand, eachPatternOperand} from '../HIR/visitors';
35
-import {empty, Stack} from '../Utils/Stack';
36
-import {assertExhaustive, Iterable_some} from '../Utils/utils';
37
-import {
38
- ReactiveScopeDependencyTree,
39
- ReactiveScopePropertyDependency,
40
-} from './DeriveMinimalDependencies';
41
-import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
42
-
43
-/*
44
- * Infers the dependencies of each scope to include variables whose values
45
- * are non-stable and created prior to the start of the scope. Also propagates
46
- * dependencies upwards, so that parent scope dependencies are the union of
47
- * their direct dependencies and those of their child scopes.
48
- */
49
-export function propagateScopeDependencies(fn: ReactiveFunction): void {
50
- const escapingTemporaries: TemporariesUsedOutsideDefiningScope = {
51
- declarations: new Map(),
52
- usedOutsideDeclaringScope: new Set(),
53
- };
54
- visitReactiveFunction(fn, new FindPromotedTemporaries(), escapingTemporaries);
55
-
56
- const context = new Context(escapingTemporaries.usedOutsideDeclaringScope);
57
- for (const param of fn.params) {
58
- if (param.kind === 'Identifier') {
59
- context.declare(param.identifier, {
60
- id: makeInstructionId(0),
61
- scope: empty(),
62
- });
63
- } else {
64
- context.declare(param.place.identifier, {
65
- id: makeInstructionId(0),
66
- scope: empty(),
67
- });
68
- }
69
- }
70
- visitReactiveFunction(fn, new PropagationVisitor(fn.env), context);
71
-}
72
-
73
-type TemporariesUsedOutsideDefiningScope = {
74
- /*
75
- * tracks all relevant temporary declarations (currently LoadLocal and PropertyLoad)
76
- * and the scope where they are defined
77
- */
78
- declarations: Map<DeclarationId, ScopeId>;
79
- // temporaries used outside of their defining scope
80
- usedOutsideDeclaringScope: Set<DeclarationId>;
81
-};
82
-class FindPromotedTemporaries extends ReactiveFunctionVisitor<TemporariesUsedOutsideDefiningScope> {
83
- scopes: Array<ScopeId> = [];
84
-
85
- override visitScope(
86
- scope: ReactiveScopeBlock,
87
- state: TemporariesUsedOutsideDefiningScope,
88
- ): void {
89
- this.scopes.push(scope.scope.id);
90
- this.traverseScope(scope, state);
91
- this.scopes.pop();
92
- }
93
-
94
- override visitInstruction(
95
- instruction: ReactiveInstruction,
96
- state: TemporariesUsedOutsideDefiningScope,
97
- ): void {
98
- // Visit all places first, then record temporaries which may need to be promoted
99
- this.traverseInstruction(instruction, state);
100
-
101
- const scope = this.scopes.at(-1);
102
- if (instruction.lvalue === null || scope === undefined) {
103
- return;
104
- }
105
- switch (instruction.value.kind) {
106
- case 'LoadLocal':
107
- case 'LoadContext':
108
- case 'PropertyLoad': {
109
- state.declarations.set(
110
- instruction.lvalue.identifier.declarationId,
111
- scope,
112
- );
113
- break;
114
- }
115
- default: {
116
- break;
117
- }
118
- }
119
- }
120
-
121
- override visitPlace(
122
- _id: InstructionId,
123
- place: Place,
124
- state: TemporariesUsedOutsideDefiningScope,
125
- ): void {
126
- const declaringScope = state.declarations.get(
127
- place.identifier.declarationId,
128
- );
129
- if (declaringScope === undefined) {
130
- return;
131
- }
132
- if (this.scopes.indexOf(declaringScope) === -1) {
133
- // Declaring scope is not active === used outside declaring scope
134
- state.usedOutsideDeclaringScope.add(place.identifier.declarationId);
135
- }
136
- }
137
-}
138
-
139
-type DeclMap = Map<DeclarationId, Decl>;
140
-type Decl = {
141
- id: InstructionId;
142
- scope: Stack<ScopeTraversalState>;
143
-};
144
-
145
-/**
146
- * TraversalState and PoisonState is used to track the poisoned state of a scope.
147
- *
148
- * A scope is poisoned when either of these conditions hold:
149
- * - one of its own nested blocks is a jump target (for break/continues)
150
- * - it is a outermost scope and contains a throw / return
151
- *
152
- * When a scope is poisoned, all dependencies (from instructions and inner scopes)
153
- * are added as conditionally accessed.
154
- */
155
-type ScopeTraversalState = {
156
- value: ReactiveScope;
157
- ownBlocks: Stack<BlockId>;
158
-};
159
-
160
-class PoisonState {
161
- poisonedBlocks: Set<BlockId> = new Set();
162
- poisonedScopes: Set<ScopeId> = new Set();
163
- isPoisoned: boolean = false;
164
-
165
- constructor(
166
- poisonedBlocks: Set<BlockId>,
167
- poisonedScopes: Set<ScopeId>,
168
- isPoisoned: boolean,
169
- ) {
170
- this.poisonedBlocks = poisonedBlocks;
171
- this.poisonedScopes = poisonedScopes;
172
- this.isPoisoned = isPoisoned;
173
- }
174
-
175
- clone(): PoisonState {
176
- return new PoisonState(
177
- new Set(this.poisonedBlocks),
178
- new Set(this.poisonedScopes),
179
- this.isPoisoned,
180
- );
181
- }
182
-
183
- take(other: PoisonState): PoisonState {
184
- const copy = new PoisonState(
185
- this.poisonedBlocks,
186
- this.poisonedScopes,
187
- this.isPoisoned,
188
- );
189
- this.poisonedBlocks = other.poisonedBlocks;
190
- this.poisonedScopes = other.poisonedScopes;
191
- this.isPoisoned = other.isPoisoned;
192
- return copy;
193
- }
194
-
195
- merge(
196
- others: Array<PoisonState>,
197
- currentScope: ScopeTraversalState | null,
198
- ): void {
199
- for (const other of others) {
200
- for (const id of other.poisonedBlocks) {
201
- this.poisonedBlocks.add(id);
202
- }
203
- for (const id of other.poisonedScopes) {
204
- this.poisonedScopes.add(id);
205
- }
206
- }
207
- this.#invalidate(currentScope);
208
- }
209
-
210
- #invalidate(currentScope: ScopeTraversalState | null): void {
211
- if (currentScope != null) {
212
- if (this.poisonedScopes.has(currentScope.value.id)) {
213
- this.isPoisoned = true;
214
- return;
215
- } else if (
216
- currentScope.ownBlocks.find(blockId => this.poisonedBlocks.has(blockId))
217
- ) {
218
- this.isPoisoned = true;
219
- return;
220
- }
221
- }
222
- this.isPoisoned = false;
223
- }
224
-
225
- /**
226
- * Mark a block or scope as poisoned and update the `isPoisoned` flag.
227
- *
228
- * @param targetBlock id of the block which ends non-linear control flow.
229
- * For a break/continue instruction, this is the target block.
230
- * Throw and return instructions have no target and will poison the earliest
231
- * active scope
232
- */
233
- addPoisonTarget(
234
- target: BlockId | null,
235
- activeScopes: Stack<ScopeTraversalState>,
236
- ): void {
237
- const currentScope = activeScopes.value;
238
- if (target == null && currentScope != null) {
239
- let cursor = activeScopes;
240
- while (true) {
241
- const next = cursor.pop();
242
- if (next.value == null) {
243
- const poisonedScope = cursor.value!.value.id;
244
- this.poisonedScopes.add(poisonedScope);
245
- if (poisonedScope === currentScope?.value.id) {
246
- this.isPoisoned = true;
247
- }
248
- break;
249
- } else {
250
- cursor = next;
251
- }
252
- }
253
- } else if (target != null) {
254
- this.poisonedBlocks.add(target);
255
- if (
256
- !this.isPoisoned &&
257
- currentScope?.ownBlocks.find(blockId => blockId === target)
258
- ) {
259
- this.isPoisoned = true;
260
- }
261
- }
262
- }
263
-
264
- /**
265
- * Invoked during traversal when a poisoned scope becomes inactive
266
- * @param id
267
- * @param currentScope
268
- */
269
- removeMaybePoisonedScope(
270
- id: ScopeId,
271
- currentScope: ScopeTraversalState | null,
272
- ): void {
273
- this.poisonedScopes.delete(id);
274
- this.#invalidate(currentScope);
275
- }
276
-
277
- removeMaybePoisonedBlock(
278
- id: BlockId,
279
- currentScope: ScopeTraversalState | null,
280
- ): void {
281
- this.poisonedBlocks.delete(id);
282
- this.#invalidate(currentScope);
283
- }
284
-}
285
-
286
-class Context {
287
- #temporariesUsedOutsideScope: Set<DeclarationId>;
288
- #declarations: DeclMap = new Map();
289
- #reassignments: Map<Identifier, Decl> = new Map();
290
- // Reactive dependencies used in the current reactive scope.
291
- #dependencies: ReactiveScopeDependencyTree =
292
- new ReactiveScopeDependencyTree();
293
- /*
294
- * We keep a sidemap for temporaries created by PropertyLoads, and do
295
- * not store any control flow (i.e. #inConditionalWithinScope) here.
296
- * - a ReactiveScope (A) containing a PropertyLoad may differ from the
297
- * ReactiveScope (B) that uses the produced temporary.
298
- * - codegen will inline these PropertyLoads back into scope (B)
299
- */
300
- #properties: Map<Identifier, ReactiveScopePropertyDependency> = new Map();
301
- #temporaries: Map<Identifier, Place> = new Map();
302
- #inConditionalWithinScope: boolean = false;
303
- /*
304
- * Reactive dependencies used unconditionally in the current conditional.
305
- * Composed of dependencies:
306
- * - directly accessed within block (added in visitDep)
307
- * - accessed by all cfg branches (added through promoteDeps)
308
- */
309
- #depsInCurrentConditional: ReactiveScopeDependencyTree =
310
- new ReactiveScopeDependencyTree();
311
- #scopes: Stack<ScopeTraversalState> = empty();
312
- poisonState: PoisonState = new PoisonState(new Set(), new Set(), false);
313
-
314
- constructor(temporariesUsedOutsideScope: Set<DeclarationId>) {
315
- this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope;
316
- }
317
-
318
- enter(scope: ReactiveScope, fn: () => void): Set<ReactiveScopeDependency> {
319
- // Save context of previous scope
320
- const prevInConditional = this.#inConditionalWithinScope;
321
- const previousDependencies = this.#dependencies;
322
- const prevDepsInConditional: ReactiveScopeDependencyTree | null = this
323
- .isPoisoned
324
- ? this.#depsInCurrentConditional
325
- : null;
326
- if (prevDepsInConditional != null) {
327
- this.#depsInCurrentConditional = new ReactiveScopeDependencyTree();
328
- }
329
-
330
- /*
331
- * Set context for new scope
332
- * A nested scope should add all deps it directly uses as its own
333
- * unconditional deps, regardless of whether the nested scope is itself
334
- * within a conditional
335
- */
336
- const scopedDependencies = new ReactiveScopeDependencyTree();
337
- this.#inConditionalWithinScope = false;
338
- this.#dependencies = scopedDependencies;
339
- this.#scopes = this.#scopes.push({
340
- value: scope,
341
- ownBlocks: empty(),
342
- });
343
- this.poisonState.isPoisoned = false;
344
-
345
- fn();
346
-
347
- // Restore context of previous scope
348
- this.#scopes = this.#scopes.pop();
349
- this.poisonState.removeMaybePoisonedScope(scope.id, this.#scopes.value);
350
-
351
- this.#dependencies = previousDependencies;
352
- this.#inConditionalWithinScope = prevInConditional;
353
-
354
- // Derive minimal dependencies now, since next line may mutate scopedDependencies
355
- const minInnerScopeDependencies =
356
- scopedDependencies.deriveMinimalDependencies();
357
-
358
- /*
359
- * propagate dependencies upward using the same rules as normal dependency
360
- * collection. child scopes may have dependencies on values created within
361
- * the outer scope, which necessarily cannot be dependencies of the outer
362
- * scope
363
- */
364
- this.#dependencies.addDepsFromInnerScope(
365
- scopedDependencies,
366
- this.#inConditionalWithinScope || this.isPoisoned,
367
- this.#checkValidDependency.bind(this),
368
- );
369
-
370
- if (prevDepsInConditional != null) {
371
- // Outer scope is poisoned
372
- prevDepsInConditional.addDepsFromInnerScope(
373
- this.#depsInCurrentConditional,
374
- true,
375
- this.#checkValidDependency.bind(this),
376
- );
377
- this.#depsInCurrentConditional = prevDepsInConditional;
378
- }
379
-
380
- return minInnerScopeDependencies;
381
- }
382
-
383
- isUsedOutsideDeclaringScope(place: Place): boolean {
384
- return this.#temporariesUsedOutsideScope.has(
385
- place.identifier.declarationId,
386
- );
387
- }
388
-
389
- /*
390
- * Prints dependency tree to string for debugging.
391
- * @param includeAccesses
392
- * @returns string representation of DependencyTree
393
- */
394
- printDeps(includeAccesses: boolean = false): string {
395
- return this.#dependencies.printDeps(includeAccesses);
396
- }
397
-
398
- /*
399
- * We track and return unconditional accesses / deps within this conditional.
400
- * If an object property is always used (i.e. in every conditional path), we
401
- * want to promote it to an unconditional access / dependency.
402
- *
403
- * The caller of `enterConditional` is responsible determining for promotion.
404
- * i.e. call promoteDepsFromExhaustiveConditionals to merge returned results.
405
- *
406
- * e.g. we want to mark props.a.b as an unconditional dep here
407
- * if (foo(...)) {
408
- * access(props.a.b);
409
- * } else {
410
- * access(props.a.b);
411
- * }
412
- */
413
- enterConditional(fn: () => void): ReactiveScopeDependencyTree {
414
- const prevInConditional = this.#inConditionalWithinScope;
415
- const prevUncondAccessed = this.#depsInCurrentConditional;
416
- this.#inConditionalWithinScope = true;
417
- this.#depsInCurrentConditional = new ReactiveScopeDependencyTree();
418
- fn();
419
- const result = this.#depsInCurrentConditional;
420
- this.#inConditionalWithinScope = prevInConditional;
421
- this.#depsInCurrentConditional = prevUncondAccessed;
422
- return result;
423
- }
424
-
425
- /*
426
- * Add dependencies from exhaustive CFG paths into the current ReactiveDeps
427
- * tree. If a property is used in every CFG path, it is promoted to an
428
- * unconditional access / dependency here.
429
- * @param depsInConditionals
430
- */
431
- promoteDepsFromExhaustiveConditionals(
432
- depsInConditionals: Array<ReactiveScopeDependencyTree>,
433
- ): void {
434
- this.#dependencies.promoteDepsFromExhaustiveConditionals(
435
- depsInConditionals,
436
- );
437
- this.#depsInCurrentConditional.promoteDepsFromExhaustiveConditionals(
438
- depsInConditionals,
439
- );
440
- }
441
-
442
- /*
443
- * Records where a value was declared, and optionally, the scope where the value originated from.
444
- * This is later used to determine if a dependency should be added to a scope; if the current
445
- * scope we are visiting is the same scope where the value originates, it can't be a dependency
446
- * on itself.
447
- */
448
- declare(identifier: Identifier, decl: Decl): void {
449
- if (!this.#declarations.has(identifier.declarationId)) {
450
- this.#declarations.set(identifier.declarationId, decl);
451
- }
452
- this.#reassignments.set(identifier, decl);
453
- }
454
-
455
- declareTemporary(lvalue: Place, place: Place): void {
456
- this.#temporaries.set(lvalue.identifier, place);
457
- }
458
-
459
- resolveTemporary(place: Place): Place {
460
- return this.#temporaries.get(place.identifier) ?? place;
461
- }
462
-
463
- #getProperty(
464
- object: Place,
465
- property: string,
466
- optional: boolean,
467
- ): ReactiveScopePropertyDependency {
468
- const resolvedObject = this.resolveTemporary(object);
469
- const resolvedDependency = this.#properties.get(resolvedObject.identifier);
470
- let objectDependency: ReactiveScopePropertyDependency;
471
- /*
472
- * (1) Create the base property dependency as either a LoadLocal (from a temporary)
473
- * or a deep copy of an existing property dependency.
474
- */
475
- if (resolvedDependency === undefined) {
476
- objectDependency = {
477
- identifier: resolvedObject.identifier,
478
- path: [],
479
- };
480
- } else {
481
- objectDependency = {
482
- identifier: resolvedDependency.identifier,
483
- path: [...resolvedDependency.path],
484
- };
485
- }
486
-
487
- objectDependency.path.push({property, optional});
488
-
489
- return objectDependency;
490
- }
491
-
492
- declareProperty(
493
- lvalue: Place,
494
- object: Place,
495
- property: string,
496
- optional: boolean,
497
- ): void {
498
- const nextDependency = this.#getProperty(object, property, optional);
499
- this.#properties.set(lvalue.identifier, nextDependency);
500
- }
501
-
502
- // Checks if identifier is a valid dependency in the current scope
503
- #checkValidDependency(maybeDependency: ReactiveScopeDependency): boolean {
504
- // ref.current access is not a valid dep
505
- if (
506
- isUseRefType(maybeDependency.identifier) &&
507
- maybeDependency.path.at(0)?.property === 'current'
508
- ) {
509
- return false;
510
- }
511
-
512
- // ref value is not a valid dep
513
- if (isRefValueType(maybeDependency.identifier)) {
514
- return false;
515
- }
516
-
517
- /*
518
- * object methods are not deps because they will be codegen'ed back in to
519
- * the object literal.
520
- */
521
- if (isObjectMethodType(maybeDependency.identifier)) {
522
- return false;
523
- }
524
-
525
- const identifier = maybeDependency.identifier;
526
- /*
527
- * If this operand is used in a scope, has a dynamic value, and was defined
528
- * before this scope, then its a dependency of the scope.
529
- */
530
- const currentDeclaration =
531
- this.#reassignments.get(identifier) ??
532
- this.#declarations.get(identifier.declarationId);
533
- const currentScope = this.currentScope.value?.value;
534
- return (
535
- currentScope != null &&
536
- currentDeclaration !== undefined &&
537
- currentDeclaration.id < currentScope.range.start &&
538
- (currentDeclaration.scope == null ||
539
- currentDeclaration.scope.value?.value !== currentScope)
540
- );
541
- }
542
-
543
- #isScopeActive(scope: ReactiveScope): boolean {
544
- if (this.#scopes === null) {
545
- return false;
546
- }
547
- return this.#scopes.find(state => state.value === scope);
548
- }
549
-
550
- get currentScope(): Stack<ScopeTraversalState> {
551
- return this.#scopes;
552
- }
553
-
554
- get isPoisoned(): boolean {
555
- return this.poisonState.isPoisoned;
556
- }
557
-
558
- visitOperand(place: Place): void {
559
- const resolved = this.resolveTemporary(place);
560
- /*
561
- * if this operand is a temporary created for a property load, try to resolve it to
562
- * the expanded Place. Fall back to using the operand as-is.
563
- */
564
-
565
- let dependency: ReactiveScopePropertyDependency = {
566
- identifier: resolved.identifier,
567
- path: [],
568
- };
569
- if (resolved.identifier.name === null) {
570
- const propertyDependency = this.#properties.get(resolved.identifier);
571
- if (propertyDependency !== undefined) {
572
- dependency = {...propertyDependency};
573
- }
574
- }
575
- this.visitDependency(dependency);
576
- }
577
-
578
- visitProperty(object: Place, property: string, optional: boolean): void {
579
- const nextDependency = this.#getProperty(object, property, optional);
580
- this.visitDependency(nextDependency);
581
- }
582
-
583
- visitDependency(maybeDependency: ReactiveScopePropertyDependency): void {
584
- /*
585
- * Any value used after its originally defining scope has concluded must be added as an
586
- * output of its defining scope. Regardless of whether its a const or not,
587
- * some later code needs access to the value. If the current
588
- * scope we are visiting is the same scope where the value originates, it can't be a dependency
589
- * on itself.
590
- */
591
-
592
- /*
593
- * if originalDeclaration is undefined here, then this is a free var
594
- * (all other decls e.g. `let x;` should be initialized in BuildHIR)
595
- */
596
- const originalDeclaration = this.#declarations.get(
597
- maybeDependency.identifier.declarationId,
598
- );
599
- if (
600
- originalDeclaration !== undefined &&
601
- originalDeclaration.scope.value !== null
602
- ) {
603
- originalDeclaration.scope.each(scope => {
604
- if (
605
- !this.#isScopeActive(scope.value) &&
606
- // TODO LeaveSSA: key scope.declarations by DeclarationId
607
- !Iterable_some(
608
- scope.value.declarations.values(),
609
- decl =>
610
- decl.identifier.declarationId ===
611
- maybeDependency.identifier.declarationId,
612
- )
613
- ) {
614
- scope.value.declarations.set(maybeDependency.identifier.id, {
615
- identifier: maybeDependency.identifier,
616
- scope: originalDeclaration.scope.value!.value,
617
- });
618
- }
619
- });
620
- }
621
-
622
- if (this.#checkValidDependency(maybeDependency)) {
623
- const isPoisoned = this.isPoisoned;
624
- this.#depsInCurrentConditional.add(maybeDependency, isPoisoned);
625
- /*
626
- * Add info about this dependency to the existing tree
627
- * We do not try to join/reduce dependencies here due to missing info
628
- */
629
- this.#dependencies.add(
630
- maybeDependency,
631
- this.#inConditionalWithinScope || isPoisoned,
632
- );
633
- }
634
- }
635
-
636
- /*
637
- * Record a variable that is declared in some other scope and that is being reassigned in the
638
- * current one as a {@link ReactiveScope.reassignments}
639
- */
640
- visitReassignment(place: Place): void {
641
- const currentScope = this.currentScope.value?.value;
642
- if (
643
- currentScope != null &&
644
- !Iterable_some(
645
- currentScope.reassignments,
646
- identifier =>
647
- identifier.declarationId === place.identifier.declarationId,
648
- ) &&
649
- this.#checkValidDependency({identifier: place.identifier, path: []})
650
- ) {
651
- // TODO LeaveSSA: scope.reassignments should be keyed by declarationid
652
- currentScope.reassignments.add(place.identifier);
653
- }
654
- }
655
-
656
- pushLabeledBlock(id: BlockId): void {
657
- const currentScope = this.#scopes.value;
658
- if (currentScope != null) {
659
- currentScope.ownBlocks = currentScope.ownBlocks.push(id);
660
- }
661
- }
662
- popLabeledBlock(id: BlockId): void {
663
- const currentScope = this.#scopes.value;
664
- if (currentScope != null) {
665
- const last = currentScope.ownBlocks.value;
666
- currentScope.ownBlocks = currentScope.ownBlocks.pop();
667
-
668
- CompilerError.invariant(last != null && last === id, {
669
- reason: '[PropagateScopeDependencies] Misformed block stack',
670
- loc: GeneratedSource,
671
- });
672
- }
673
- this.poisonState.removeMaybePoisonedBlock(id, currentScope);
674
- }
675
-}
676
-
677
-class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
678
- env: Environment;
679
-
680
- constructor(env: Environment) {
681
- super();
682
- this.env = env;
683
- }
684
-
685
- override visitScope(scope: ReactiveScopeBlock, context: Context): void {
686
- const scopeDependencies = context.enter(scope.scope, () => {
687
- this.visitBlock(scope.instructions, context);
688
- });
689
- for (const candidateDep of scopeDependencies) {
690
- if (
691
- !Iterable_some(
692
- scope.scope.dependencies,
693
- existingDep =>
694
- existingDep.identifier.declarationId ===
695
- candidateDep.identifier.declarationId &&
696
- areEqualPaths(existingDep.path, candidateDep.path),
697
- )
698
- ) {
699
- scope.scope.dependencies.add(candidateDep);
700
- }
701
- }
702
- /*
703
- * TODO LeaveSSA: fix existing bug with duplicate deps and reassignments
704
- * see fixture ssa-cascading-eliminated-phis, note that we cache `x`
705
- * twice because its both a dep and a reassignment.
706
- *
707
- * for (const reassignment of scope.scope.reassignments) {
708
- * if (
709
- * Iterable_some(
710
- * scope.scope.dependencies.values(),
711
- * dep =>
712
- * dep.identifier.declarationId === reassignment.declarationId &&
713
- * dep.path.length === 0,
714
- * )
715
- * ) {
716
- * scope.scope.reassignments.delete(reassignment);
717
- * }
718
- * }
719
- */
720
- }
721
-
722
- override visitPrunedScope(
723
- scopeBlock: PrunedReactiveScopeBlock,
724
- context: Context,
725
- ): void {
726
- /*
727
- * NOTE: we explicitly throw away the deps, we only enter() the scope to record its
728
- * declarations
729
- */
730
- const _scopeDepdencies = context.enter(scopeBlock.scope, () => {
731
- this.visitBlock(scopeBlock.instructions, context);
732
- });
733
- }
734
-
735
- override visitInstruction(
736
- instruction: ReactiveInstruction,
737
- context: Context,
738
- ): void {
739
- const {id, value, lvalue} = instruction;
740
- this.visitInstructionValue(context, id, value, lvalue);
741
- if (lvalue == null) {
742
- return;
743
- }
744
- context.declare(lvalue.identifier, {
745
- id,
746
- scope: context.currentScope,
747
- });
748
- }
749
-
750
- extractOptionalProperty(
751
- context: Context,
752
- optionalValue: ReactiveOptionalCallValue,
753
- lvalue: Place,
754
- ): {
755
- lvalue: Place;
756
- object: Place;
757
- property: string;
758
- optional: boolean;
759
- } | null {
760
- const sequence = optionalValue.value;
761
- CompilerError.invariant(sequence.kind === 'SequenceExpression', {
762
- reason: 'Expected OptionalExpression value to be a SequenceExpression',
763
- description: `Found a \`${sequence.kind}\``,
764
- loc: sequence.loc,
765
- });
766
- /**
767
- * Base case: inner `<variable> "?." <property>`
768
- *```
769
- * <lvalue> = OptionalExpression optional=true (`optionalValue` is here)
770
- * Sequence (`sequence` is here)
771
- * t0 = LoadLocal <variable>
772
- * Sequence
773
- * t1 = PropertyLoad t0 . <property>
774
- * LoadLocal t1
775
- * ```
776
- */
777
- if (
778
- sequence.instructions.length === 1 &&
779
- sequence.instructions[0].lvalue !== null &&
780
- sequence.instructions[0].value.kind === 'LoadLocal' &&
781
- sequence.instructions[0].value.place.identifier.name !== null &&
782
- !context.isUsedOutsideDeclaringScope(sequence.instructions[0].lvalue) &&
783
- sequence.value.kind === 'SequenceExpression' &&
784
- sequence.value.instructions.length === 1 &&
785
- sequence.value.instructions[0].value.kind === 'PropertyLoad' &&
786
- sequence.value.instructions[0].value.object.identifier.id ===
787
- sequence.instructions[0].lvalue.identifier.id &&
788
- sequence.value.instructions[0].lvalue !== null &&
789
- sequence.value.value.kind === 'LoadLocal' &&
790
- sequence.value.value.place.identifier.id ===
791
- sequence.value.instructions[0].lvalue.identifier.id
792
- ) {
793
- context.declareTemporary(
794
- sequence.instructions[0].lvalue,
795
- sequence.instructions[0].value.place,
796
- );
797
- const propertyLoad = sequence.value.instructions[0].value;
798
- return {
799
- lvalue,
800
- object: propertyLoad.object,
801
- property: propertyLoad.property,
802
- optional: optionalValue.optional,
803
- };
804
- }
805
- /**
806
- * Base case 2: inner `<variable> "." <property1> "?." <property2>
807
- * ```
808
- * <lvalue> = OptionalExpression optional=true (`optionalValue` is here)
809
- * Sequence (`sequence` is here)
810
- * t0 = Sequence
811
- * t1 = LoadLocal <variable>
812
- * ... // see note
813
- * PropertyLoad t1 . <property1>
814
- * [46] Sequence
815
- * t2 = PropertyLoad t0 . <property2>
816
- * [46] LoadLocal t2
817
- * ```
818
- *
819
- * Note that it's possible to have additional inner chained non-optional
820
- * property loads at "...", from an expression like `a?.b.c.d.e`. We could
821
- * expand to support this case by relaxing the check on the inner sequence
822
- * length, ensuring all instructions after the first LoadLocal are PropertyLoad
823
- * and then iterating to ensure that the lvalue of the previous is always
824
- * the object of the next PropertyLoad, w the final lvalue as the object
825
- * of the sequence.value's object.
826
- *
827
- * But this case is likely rare in practice, usually once you're optional
828
- * chaining all property accesses are optional (not `a?.b.c` but `a?.b?.c`).
829
- * Also, HIR-based PropagateScopeDeps will handle this case so it doesn't
830
- * seem worth it to optimize for that edge-case here.
831
- */
832
- if (
833
- sequence.instructions.length === 1 &&
834
- sequence.instructions[0].lvalue !== null &&
835
- sequence.instructions[0].value.kind === 'SequenceExpression' &&
836
- sequence.instructions[0].value.instructions.length === 1 &&
837
- sequence.instructions[0].value.instructions[0].lvalue !== null &&
838
- sequence.instructions[0].value.instructions[0].value.kind ===
839
- 'LoadLocal' &&
840
- sequence.instructions[0].value.instructions[0].value.place.identifier
841
- .name !== null &&
842
- !context.isUsedOutsideDeclaringScope(
843
- sequence.instructions[0].value.instructions[0].lvalue,
844
- ) &&
845
- sequence.instructions[0].value.value.kind === 'PropertyLoad' &&
846
- sequence.instructions[0].value.value.object.identifier.id ===
847
- sequence.instructions[0].value.instructions[0].lvalue.identifier.id &&
848
- sequence.value.kind === 'SequenceExpression' &&
849
- sequence.value.instructions.length === 1 &&
850
- sequence.value.instructions[0].lvalue !== null &&
851
- sequence.value.instructions[0].value.kind === 'PropertyLoad' &&
852
- sequence.value.instructions[0].value.object.identifier.id ===
853
- sequence.instructions[0].lvalue.identifier.id &&
854
- sequence.value.value.kind === 'LoadLocal' &&
855
- sequence.value.value.place.identifier.id ===
856
- sequence.value.instructions[0].lvalue.identifier.id
857
- ) {
858
- // LoadLocal <variable>
859
- context.declareTemporary(
860
- sequence.instructions[0].value.instructions[0].lvalue,
861
- sequence.instructions[0].value.instructions[0].value.place,
862
- );
863
- // PropertyLoad <variable> . <property1> (the inner non-optional property)
864
- context.declareProperty(
865
- sequence.instructions[0].lvalue,
866
- sequence.instructions[0].value.value.object,
867
- sequence.instructions[0].value.value.property,
868
- false,
869
- );
870
- const propertyLoad = sequence.value.instructions[0].value;
871
- return {
872
- lvalue,
873
- object: propertyLoad.object,
874
- property: propertyLoad.property,
875
- optional: optionalValue.optional,
876
- };
877
- }
878
-
879
- /**
880
- * Composed case:
881
- * - `<base-case> "." or "?." <property>`
882
- * - `<composed-case> "." or "?>" <property>`
883
- *
884
- * This case is convoluted, note how `t0` appears as an lvalue *twice*
885
- * and then is an operand of an intermediate LoadLocal and then the
886
- * object of the final PropertyLoad:
887
- *
888
- * ```
889
- * <lvalue> = OptionalExpression optional=false (`optionalValue` is here)
890
- * Sequence (`sequence` is here)
891
- * t0 = Sequence
892
- * t0 =
893
- * <nested>
894
- * LoadLocal t0
895
- * Sequence
896
- * t1 = PropertyLoad t0. <property>
897
- * LoadLocal t1
898
- * ```
899
- */
900
- if (
901
- sequence.instructions.length === 1 &&
902
- sequence.instructions[0].value.kind === 'SequenceExpression' &&
903
- sequence.instructions[0].value.instructions.length === 1 &&
904
- sequence.instructions[0].value.instructions[0].lvalue !== null &&
905
- sequence.instructions[0].value.instructions[0].value.kind ===
906
- 'OptionalExpression' &&
907
- sequence.instructions[0].value.value.kind === 'LoadLocal' &&
908
- sequence.instructions[0].value.value.place.identifier.id ===
909
- sequence.instructions[0].value.instructions[0].lvalue.identifier.id &&
910
- sequence.value.kind === 'SequenceExpression' &&
911
- sequence.value.instructions.length === 1 &&
912
- sequence.value.instructions[0].lvalue !== null &&
913
- sequence.value.instructions[0].value.kind === 'PropertyLoad' &&
914
- sequence.value.instructions[0].value.object.identifier.id ===
915
- sequence.instructions[0].value.value.place.identifier.id &&
916
- sequence.value.value.kind === 'LoadLocal' &&
917
- sequence.value.value.place.identifier.id ===
918
- sequence.value.instructions[0].lvalue.identifier.id
919
- ) {
920
- const {lvalue: innerLvalue, value: innerOptional} =
921
- sequence.instructions[0].value.instructions[0];
922
- const innerProperty = this.extractOptionalProperty(
923
- context,
924
- innerOptional,
925
- innerLvalue,
926
- );
927
- if (innerProperty === null) {
928
- return null;
929
- }
930
- context.declareProperty(
931
- innerProperty.lvalue,
932
- innerProperty.object,
933
- innerProperty.property,
934
- innerProperty.optional,
935
- );
936
- const propertyLoad = sequence.value.instructions[0].value;
937
- return {
938
- lvalue,
939
- object: propertyLoad.object,
940
- property: propertyLoad.property,
941
- optional: optionalValue.optional,
942
- };
943
- }
944
- return null;
945
- }
946
-
947
- visitOptionalExpression(
948
- context: Context,
949
- id: InstructionId,
950
- value: ReactiveOptionalCallValue,
951
- lvalue: Place | null,
952
- ): void {
953
- /**
954
- * If this is the first optional=true optional in a recursive OptionalExpression
955
- * subtree, we check to see if the subtree is of the form:
956
- * ```
957
- * NestedOptional =
958
- * `<variable> . / ?. <property>`
959
- * `<nested-optional> . / ?. <property>`
960
- * ```
961
- *
962
- * Ie strictly a chain like `foo?.bar?.baz` or `a?.b.c`. If the subtree contains
963
- * any other types of expressions - for example `foo?.[makeKey(a)]` - then this
964
- * will return null and we'll go to the default handling below.
965
- *
966
- * If the tree does match the NestedOptional shape, then we'll have recorded
967
- * a sequence of declareProperty calls, and the final visitProperty call here
968
- * will record that optional chain as a dependency (since we know it's about
969
- * to be referenced via its lvalue which is non-null).
970
- */
971
- if (
972
- lvalue !== null &&
973
- value.optional &&
974
- this.env.config.enableOptionalDependencies
975
- ) {
976
- const inner = this.extractOptionalProperty(context, value, lvalue);
977
- if (inner !== null) {
978
- context.visitProperty(inner.object, inner.property, inner.optional);
979
- return;
980
- }
981
- }
982
-
983
- // Otherwise we treat everything after the optional as conditional
984
- const inner = value.value;
985
- /*
986
- * OptionalExpression value is a SequenceExpression where the instructions
987
- * represent the code prior to the `?` and the final value represents the
988
- * conditional code that follows.
989
- */
990
- CompilerError.invariant(inner.kind === 'SequenceExpression', {
991
- reason: 'Expected OptionalExpression value to be a SequenceExpression',
992
- description: `Found a \`${value.kind}\``,
993
- loc: value.loc,
994
- suggestions: null,
995
- });
996
- // Instructions are the unconditionally executed portion before the `?`
997
- for (const instr of inner.instructions) {
998
- this.visitInstruction(instr, context);
999
- }
1000
- // The final value is the conditional portion following the `?`
1001
- context.enterConditional(() => {
1002
- this.visitReactiveValue(context, id, inner.value, null);
1003
- });
1004
- }
1005
-
1006
- visitReactiveValue(
1007
- context: Context,
1008
- id: InstructionId,
1009
- value: ReactiveValue,
1010
- lvalue: Place | null,
1011
- ): void {
1012
- switch (value.kind) {
1013
- case 'OptionalExpression': {
1014
- this.visitOptionalExpression(context, id, value, lvalue);
1015
- break;
1016
- }
1017
- case 'LogicalExpression': {
1018
- this.visitReactiveValue(context, id, value.left, null);
1019
- context.enterConditional(() => {
1020
- this.visitReactiveValue(context, id, value.right, null);
1021
- });
1022
- break;
1023
- }
1024
- case 'ConditionalExpression': {
1025
- this.visitReactiveValue(context, id, value.test, null);
1026
-
1027
- const consequentDeps = context.enterConditional(() => {
1028
- this.visitReactiveValue(context, id, value.consequent, null);
1029
- });
1030
- const alternateDeps = context.enterConditional(() => {
1031
- this.visitReactiveValue(context, id, value.alternate, null);
1032
- });
1033
- context.promoteDepsFromExhaustiveConditionals([
1034
- consequentDeps,
1035
- alternateDeps,
1036
- ]);
1037
- break;
1038
- }
1039
- case 'SequenceExpression': {
1040
- for (const instr of value.instructions) {
1041
- this.visitInstruction(instr, context);
1042
- }
1043
- this.visitInstructionValue(context, id, value.value, null);
1044
- break;
1045
- }
1046
- case 'FunctionExpression': {
1047
- if (this.env.config.enableTreatFunctionDepsAsConditional) {
1048
- context.enterConditional(() => {
1049
- for (const operand of eachInstructionValueOperand(value)) {
1050
- context.visitOperand(operand);
1051
- }
1052
- });
1053
- } else {
1054
- for (const operand of eachInstructionValueOperand(value)) {
1055
- context.visitOperand(operand);
1056
- }
1057
- }
1058
- break;
1059
- }
1060
- case 'ReactiveFunctionValue': {
1061
- CompilerError.invariant(false, {
1062
- reason: `Unexpected ReactiveFunctionValue`,
1063
- loc: value.loc,
1064
- description: null,
1065
- suggestions: null,
1066
- });
1067
- }
1068
- default: {
1069
- for (const operand of eachInstructionValueOperand(value)) {
1070
- context.visitOperand(operand);
1071
- }
1072
- }
1073
- }
1074
- }
1075
-
1076
- visitInstructionValue(
1077
- context: Context,
1078
- id: InstructionId,
1079
- value: ReactiveValue,
1080
- lvalue: Place | null,
1081
- ): void {
1082
- if (value.kind === 'LoadLocal' && lvalue !== null) {
1083
- if (
1084
- value.place.identifier.name !== null &&
1085
- lvalue.identifier.name === null &&
1086
- !context.isUsedOutsideDeclaringScope(lvalue)
1087
- ) {
1088
- context.declareTemporary(lvalue, value.place);
1089
- } else {
1090
- context.visitOperand(value.place);
1091
- }
1092
- } else if (value.kind === 'PropertyLoad') {
1093
- if (lvalue !== null && !context.isUsedOutsideDeclaringScope(lvalue)) {
1094
- context.declareProperty(lvalue, value.object, value.property, false);
1095
- } else {
1096
- context.visitProperty(value.object, value.property, false);
1097
- }
1098
- } else if (value.kind === 'StoreLocal') {
1099
- context.visitOperand(value.value);
1100
- if (value.lvalue.kind === InstructionKind.Reassign) {
1101
- context.visitReassignment(value.lvalue.place);
1102
- }
1103
- context.declare(value.lvalue.place.identifier, {
1104
- id,
1105
- scope: context.currentScope,
1106
- });
1107
- } else if (
1108
- value.kind === 'DeclareLocal' ||
1109
- value.kind === 'DeclareContext'
1110
- ) {
1111
- /*
1112
- * Some variables may be declared and never initialized. We need
1113
- * to retain (and hoist) these declarations if they are included
1114
- * in a reactive scope. One approach is to simply add all `DeclareLocal`s
1115
- * as scope declarations.
1116
- */
1117
-
1118
- /*
1119
- * We add context variable declarations here, not at `StoreContext`, since
1120
- * context Store / Loads are modeled as reads and mutates to the underlying
1121
- * variable reference (instead of through intermediate / inlined temporaries)
1122
- */
1123
- context.declare(value.lvalue.place.identifier, {
1124
- id,
1125
- scope: context.currentScope,
1126
- });
1127
- } else if (value.kind === 'Destructure') {
1128
- context.visitOperand(value.value);
1129
- for (const place of eachPatternOperand(value.lvalue.pattern)) {
1130
- if (value.lvalue.kind === InstructionKind.Reassign) {
1131
- context.visitReassignment(place);
1132
- }
1133
- context.declare(place.identifier, {
1134
- id,
1135
- scope: context.currentScope,
1136
- });
1137
- }
1138
- } else {
1139
- this.visitReactiveValue(context, id, value, lvalue);
1140
- }
1141
- }
1142
-
1143
- enterTerminal(stmt: ReactiveTerminalStatement, context: Context): void {
1144
- if (stmt.label != null) {
1145
- context.pushLabeledBlock(stmt.label.id);
1146
- }
1147
- const terminal = stmt.terminal;
1148
- switch (terminal.kind) {
1149
- case 'continue':
1150
- case 'break': {
1151
- context.poisonState.addPoisonTarget(
1152
- terminal.target,
1153
- context.currentScope,
1154
- );
1155
- break;
1156
- }
1157
- case 'throw':
1158
- case 'return': {
1159
- context.poisonState.addPoisonTarget(null, context.currentScope);
1160
- break;
1161
- }
1162
- }
1163
- }
1164
- exitTerminal(stmt: ReactiveTerminalStatement, context: Context): void {
1165
- if (stmt.label != null) {
1166
- context.popLabeledBlock(stmt.label.id);
1167
- }
1168
- }
1169
-
1170
- override visitTerminal(
1171
- stmt: ReactiveTerminalStatement,
1172
- context: Context,
1173
- ): void {
1174
- this.enterTerminal(stmt, context);
1175
- const terminal = stmt.terminal;
1176
- switch (terminal.kind) {
1177
- case 'break':
1178
- case 'continue': {
1179
- break;
1180
- }
1181
- case 'return': {
1182
- context.visitOperand(terminal.value);
1183
- break;
1184
- }
1185
- case 'throw': {
1186
- context.visitOperand(terminal.value);
1187
- break;
1188
- }
1189
- case 'for': {
1190
- this.visitReactiveValue(context, terminal.id, terminal.init, null);
1191
- this.visitReactiveValue(context, terminal.id, terminal.test, null);
1192
- context.enterConditional(() => {
1193
- this.visitBlock(terminal.loop, context);
1194
- if (terminal.update !== null) {
1195
- this.visitReactiveValue(
1196
- context,
1197
- terminal.id,
1198
- terminal.update,
1199
- null,
1200
- );
1201
- }
1202
- });
1203
- break;
1204
- }
1205
- case 'for-of': {
1206
- this.visitReactiveValue(context, terminal.id, terminal.init, null);
1207
- context.enterConditional(() => {
1208
- this.visitBlock(terminal.loop, context);
1209
- });
1210
- break;
1211
- }
1212
- case 'for-in': {
1213
- this.visitReactiveValue(context, terminal.id, terminal.init, null);
1214
- context.enterConditional(() => {
1215
- this.visitBlock(terminal.loop, context);
1216
- });
1217
- break;
1218
- }
1219
- case 'do-while': {
1220
- this.visitBlock(terminal.loop, context);
1221
- context.enterConditional(() => {
1222
- this.visitReactiveValue(context, terminal.id, terminal.test, null);
1223
- });
1224
- break;
1225
- }
1226
- case 'while': {
1227
- this.visitReactiveValue(context, terminal.id, terminal.test, null);
1228
- context.enterConditional(() => {
1229
- this.visitBlock(terminal.loop, context);
1230
- });
1231
- break;
1232
- }
1233
- case 'if': {
1234
- context.visitOperand(terminal.test);
1235
- const {consequent, alternate} = terminal;
1236
- /*
1237
- * Consequent and alternate branches are mutually exclusive,
1238
- * so we save and restore the poison state here.
1239
- */
1240
- const prevPoisonState = context.poisonState.clone();
1241
- const depsInIf = context.enterConditional(() => {
1242
- this.visitBlock(consequent, context);
1243
- });
1244
- if (alternate !== null) {
1245
- const ifPoisonState = context.poisonState.take(prevPoisonState);
1246
- const depsInElse = context.enterConditional(() => {
1247
- this.visitBlock(alternate, context);
1248
- });
1249
- context.poisonState.merge(
1250
- [ifPoisonState],
1251
- context.currentScope.value,
1252
- );
1253
- context.promoteDepsFromExhaustiveConditionals([depsInIf, depsInElse]);
1254
- }
1255
- break;
1256
- }
1257
- case 'switch': {
1258
- context.visitOperand(terminal.test);
1259
- const isDefaultOnly =
1260
- terminal.cases.length === 1 && terminal.cases[0].test == null;
1261
- if (isDefaultOnly) {
1262
- const case_ = terminal.cases[0];
1263
- if (case_.block != null) {
1264
- this.visitBlock(case_.block, context);
1265
- break;
1266
- }
1267
- }
1268
- const depsInCases = [];
1269
- let foundDefault = false;
1270
- /**
1271
- * Switch branches are mutually exclusive
1272
- */
1273
- const prevPoisonState = context.poisonState.clone();
1274
- const mutExPoisonStates: Array<PoisonState> = [];
1275
- /*
1276
- * This can underestimate unconditional accesses due to the current
1277
- * CFG representation for fallthrough. This is safe. It only
1278
- * reduces granularity of dependencies.
1279
- */
1280
- for (const {test, block} of terminal.cases) {
1281
- if (test !== null) {
1282
- context.visitOperand(test);
1283
- } else {
1284
- foundDefault = true;
1285
- }
1286
- if (block !== undefined) {
1287
- mutExPoisonStates.push(
1288
- context.poisonState.take(prevPoisonState.clone()),
1289
- );
1290
- depsInCases.push(
1291
- context.enterConditional(() => {
1292
- this.visitBlock(block, context);
1293
- }),
1294
- );
1295
- }
1296
- }
1297
- if (foundDefault) {
1298
- context.promoteDepsFromExhaustiveConditionals(depsInCases);
1299
- }
1300
- context.poisonState.merge(
1301
- mutExPoisonStates,
1302
- context.currentScope.value,
1303
- );
1304
- break;
1305
- }
1306
- case 'label': {
1307
- this.visitBlock(terminal.block, context);
1308
- break;
1309
- }
1310
- case 'try': {
1311
- this.visitBlock(terminal.block, context);
1312
- this.visitBlock(terminal.handler, context);
1313
- break;
1314
- }
1315
- default: {
1316
- assertExhaustive(
1317
- terminal,
1318
- `Unexpected terminal kind \`${(terminal as any).kind}\``,
1319
- );
1320
- }
1321
- }
1322
- this.exitTerminal(stmt, context);
1323
- }
1324
-}