main
ts 843 lines 27.3 KB
Raw
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, SourceLocation} from '..';
9 import {
10 BlockId,
11 Effect,
12 HIRFunction,
13 Identifier,
14 IdentifierId,
15 InstructionId,
16 isJsxType,
17 makeInstructionId,
18 ValueKind,
19 ValueReason,
20 Place,
21 isPrimitiveType,
22 } from '../HIR/HIR';
23 import {Environment} from '../HIR/Environment';
24 import {
25 eachInstructionLValue,
26 eachInstructionValueOperand,
27 eachTerminalOperand,
28 } from '../HIR/visitors';
29 import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
30
31 import {AliasingEffect, MutationReason} from './AliasingEffects';
32
33 /**
34 * This pass builds an abstract model of the heap and interprets the effects of the
35 * given function in order to determine the following:
36 * - The mutable ranges of all identifiers in the function
37 * - The externally-visible effects of the function, such as mutations of params and
38 * context-vars, aliasing between params/context-vars/return-value, and impure side
39 * effects.
40 * - The legacy `Effect` to store on each Place.
41 *
42 * This pass builds a data flow graph using the effects, tracking an abstract notion
43 * of "when" each effect occurs relative to the others. It then walks each mutation
44 * effect against the graph, updating the range of each node that would be reachable
45 * at the "time" that the effect occurred.
46 *
47 * This pass also validates against invalid effects: any function that is reachable
48 * by being called, or via a Render effect, is validated against mutating globals
49 * or calling impure code.
50 *
51 * Note that this function also populates the outer function's aliasing effects with
52 * any mutations that apply to its params or context variables.
53 *
54 * ## Example
55 * A function expression such as the following:
56 *
57 * ```
58 * (x) => { x.y = true }
59 * ```
60 *
61 * Would populate a `Mutate x` aliasing effect on the outer function.
62 *
63 * ## Returned Function Effects
64 *
65 * The function returns (if successful) a list of externally-visible effects.
66 * This is determined by simulating a conditional, transitive mutation against
67 * each param, context variable, and return value in turn, and seeing which other
68 * such values are affected. If they're affected, they must be captured, so we
69 * record a Capture.
70 *
71 * The only tricky bit is the return value, which could _alias_ (or even assign)
72 * one or more of the params/context-vars rather than just capturing. So we have
73 * to do a bit more tracking for returns.
74 */
75 export function inferMutationAliasingRanges(
76 fn: HIRFunction,
77 {isFunctionExpression}: {isFunctionExpression: boolean},
78 ): Array<AliasingEffect> {
79 // The set of externally-visible effects
80 const functionEffects: Array<AliasingEffect> = [];
81
82 /**
83 * Part 1: Infer mutable ranges for values. We build an abstract model of
84 * values, the alias/capture edges between them, and the set of mutations.
85 * Edges and mutations are ordered, with mutations processed against the
86 * abstract model only after it is fully constructed by visiting all blocks
87 * _and_ connecting phis. Phis are considered ordered at the time of the
88 * phi node.
89 *
90 * This should (may?) mean that mutations are able to see the full state
91 * of the graph and mark all the appropriate identifiers as mutated at
92 * the correct point, accounting for both backward and forward edges.
93 * Ie a mutation of x accounts for both values that flowed into x,
94 * and values that x flowed into.
95 */
96 const state = new AliasingState();
97 type PendingPhiOperand = {from: Place; into: Place; index: number};
98 const pendingPhis = new Map<BlockId, Array<PendingPhiOperand>>();
99 const mutations: Array<{
100 index: number;
101 id: InstructionId;
102 transitive: boolean;
103 kind: MutationKind;
104 place: Place;
105 reason: MutationReason | null;
106 }> = [];
107 const renders: Array<{index: number; place: Place}> = [];
108
109 let index = 0;
110
111 const shouldRecordErrors = !isFunctionExpression && fn.env.enableValidations;
112
113 for (const param of [...fn.params, ...fn.context, fn.returns]) {
114 const place = param.kind === 'Identifier' ? param : param.place;
115 state.create(place, {kind: 'Object'});
116 }
117 const seenBlocks = new Set<BlockId>();
118 for (const block of fn.body.blocks.values()) {
119 for (const phi of block.phis) {
120 state.create(phi.place, {kind: 'Phi'});
121 for (const [pred, operand] of phi.operands) {
122 if (!seenBlocks.has(pred)) {
123 // NOTE: annotation required to actually typecheck and not silently infer `any`
124 const blockPhis = getOrInsertWith<BlockId, Array<PendingPhiOperand>>(
125 pendingPhis,
126 pred,
127 () => [],
128 );
129 blockPhis.push({from: operand, into: phi.place, index: index++});
130 } else {
131 state.assign(index++, operand, phi.place);
132 }
133 }
134 }
135 seenBlocks.add(block.id);
136
137 for (const instr of block.instructions) {
138 if (instr.effects == null) continue;
139 for (const effect of instr.effects) {
140 if (effect.kind === 'Create') {
141 state.create(effect.into, {kind: 'Object'});
142 } else if (effect.kind === 'CreateFunction') {
143 state.create(effect.into, {
144 kind: 'Function',
145 function: effect.function.loweredFunc.func,
146 });
147 } else if (effect.kind === 'CreateFrom') {
148 state.createFrom(index++, effect.from, effect.into);
149 } else if (effect.kind === 'Assign') {
150 /**
151 * TODO: Invariant that the node is not initialized yet
152 *
153 * InferFunctionExpressionAliasingEffectSignatures currently infers
154 * Assign effects in some places that should be Alias, leading to
155 * Assign effects that reinitialize a value. The end result appears to
156 * be fine, but we should fix that inference pass so that we add the
157 * invariant here.
158 */
159 if (!state.nodes.has(effect.into.identifier)) {
160 state.create(effect.into, {kind: 'Object'});
161 }
162 state.assign(index++, effect.from, effect.into);
163 } else if (effect.kind === 'Alias') {
164 state.assign(index++, effect.from, effect.into);
165 } else if (effect.kind === 'MaybeAlias') {
166 state.maybeAlias(index++, effect.from, effect.into);
167 } else if (effect.kind === 'Capture') {
168 state.capture(index++, effect.from, effect.into);
169 } else if (
170 effect.kind === 'MutateTransitive' ||
171 effect.kind === 'MutateTransitiveConditionally'
172 ) {
173 mutations.push({
174 index: index++,
175 id: instr.id,
176 transitive: true,
177 kind:
178 effect.kind === 'MutateTransitive'
179 ? MutationKind.Definite
180 : MutationKind.Conditional,
181 reason: null,
182 place: effect.value,
183 });
184 } else if (
185 effect.kind === 'Mutate' ||
186 effect.kind === 'MutateConditionally'
187 ) {
188 mutations.push({
189 index: index++,
190 id: instr.id,
191 transitive: false,
192 kind:
193 effect.kind === 'Mutate'
194 ? MutationKind.Definite
195 : MutationKind.Conditional,
196 reason: effect.kind === 'Mutate' ? (effect.reason ?? null) : null,
197 place: effect.value,
198 });
199 } else if (
200 effect.kind === 'MutateFrozen' ||
201 effect.kind === 'MutateGlobal' ||
202 effect.kind === 'Impure'
203 ) {
204 if (shouldRecordErrors) {
205 fn.env.recordError(effect.error);
206 }
207 functionEffects.push(effect);
208 } else if (effect.kind === 'Render') {
209 renders.push({index: index++, place: effect.place});
210 functionEffects.push(effect);
211 }
212 }
213 }
214 const blockPhis = pendingPhis.get(block.id);
215 if (blockPhis != null) {
216 for (const {from, into, index} of blockPhis) {
217 state.assign(index, from, into);
218 }
219 }
220 if (block.terminal.kind === 'return') {
221 state.assign(index++, block.terminal.value, fn.returns);
222 }
223
224 if (
225 (block.terminal.kind === 'maybe-throw' ||
226 block.terminal.kind === 'return') &&
227 block.terminal.effects != null
228 ) {
229 for (const effect of block.terminal.effects) {
230 if (effect.kind === 'Alias') {
231 state.assign(index++, effect.from, effect.into);
232 } else {
233 CompilerError.invariant(effect.kind === 'Freeze', {
234 reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
235 loc: block.terminal.loc,
236 });
237 }
238 }
239 }
240 }
241
242 for (const mutation of mutations) {
243 state.mutate(
244 mutation.index,
245 mutation.place.identifier,
246 makeInstructionId(mutation.id + 1),
247 mutation.transitive,
248 mutation.kind,
249 mutation.place.loc,
250 mutation.reason,
251 shouldRecordErrors ? fn.env : null,
252 );
253 }
254 for (const render of renders) {
255 state.render(
256 render.index,
257 render.place.identifier,
258 shouldRecordErrors ? fn.env : null,
259 );
260 }
261 for (const param of [...fn.context, ...fn.params]) {
262 const place = param.kind === 'Identifier' ? param : param.place;
263
264 const node = state.nodes.get(place.identifier);
265 if (node == null) {
266 continue;
267 }
268 let mutated = false;
269 if (node.local != null) {
270 if (node.local.kind === MutationKind.Conditional) {
271 mutated = true;
272 functionEffects.push({
273 kind: 'MutateConditionally',
274 value: {...place, loc: node.local.loc},
275 });
276 } else if (node.local.kind === MutationKind.Definite) {
277 mutated = true;
278 functionEffects.push({
279 kind: 'Mutate',
280 value: {...place, loc: node.local.loc},
281 reason: node.mutationReason,
282 });
283 }
284 }
285 if (node.transitive != null) {
286 if (node.transitive.kind === MutationKind.Conditional) {
287 mutated = true;
288 functionEffects.push({
289 kind: 'MutateTransitiveConditionally',
290 value: {...place, loc: node.transitive.loc},
291 });
292 } else if (node.transitive.kind === MutationKind.Definite) {
293 mutated = true;
294 functionEffects.push({
295 kind: 'MutateTransitive',
296 value: {...place, loc: node.transitive.loc},
297 });
298 }
299 }
300 if (mutated) {
301 place.effect = Effect.Capture;
302 }
303 }
304
305 /**
306 * Part 2
307 * Add legacy operand-specific effects based on instruction effects and mutable ranges.
308 * Also fixes up operand mutable ranges, making sure that start is non-zero if the value
309 * is mutated (depended on by later passes like InferReactiveScopeVariables which uses this
310 * to filter spurious mutations of globals, which we now guard against more precisely)
311 */
312 for (const block of fn.body.blocks.values()) {
313 for (const phi of block.phis) {
314 // TODO: we don't actually set these effects today!
315 phi.place.effect = Effect.Store;
316 const isPhiMutatedAfterCreation: boolean =
317 phi.place.identifier.mutableRange.end >
318 (block.instructions.at(0)?.id ?? block.terminal.id);
319 for (const operand of phi.operands.values()) {
320 operand.effect = isPhiMutatedAfterCreation
321 ? Effect.Capture
322 : Effect.Read;
323 }
324 if (
325 isPhiMutatedAfterCreation &&
326 phi.place.identifier.mutableRange.start === 0
327 ) {
328 /*
329 * TODO: ideally we'd construct a precise start range, but what really
330 * matters is that the phi's range appears mutable (end > start + 1)
331 * so we just set the start to the previous instruction before this block
332 */
333 const firstInstructionIdOfBlock =
334 block.instructions.at(0)?.id ?? block.terminal.id;
335 phi.place.identifier.mutableRange.start = makeInstructionId(
336 firstInstructionIdOfBlock - 1,
337 );
338 }
339 }
340 for (const instr of block.instructions) {
341 for (const lvalue of eachInstructionLValue(instr)) {
342 lvalue.effect = Effect.ConditionallyMutate;
343 if (lvalue.identifier.mutableRange.start === 0) {
344 lvalue.identifier.mutableRange.start = instr.id;
345 }
346 if (lvalue.identifier.mutableRange.end === 0) {
347 lvalue.identifier.mutableRange.end = makeInstructionId(
348 Math.max(instr.id + 1, lvalue.identifier.mutableRange.end),
349 );
350 }
351 }
352 for (const operand of eachInstructionValueOperand(instr.value)) {
353 operand.effect = Effect.Read;
354 }
355 if (instr.effects == null) {
356 continue;
357 }
358 const operandEffects = new Map<IdentifierId, Effect>();
359 for (const effect of instr.effects) {
360 switch (effect.kind) {
361 case 'Assign':
362 case 'Alias':
363 case 'Capture':
364 case 'CreateFrom':
365 case 'MaybeAlias': {
366 const isMutatedOrReassigned =
367 effect.into.identifier.mutableRange.end > instr.id;
368 if (isMutatedOrReassigned) {
369 operandEffects.set(effect.from.identifier.id, Effect.Capture);
370 operandEffects.set(effect.into.identifier.id, Effect.Store);
371 } else {
372 operandEffects.set(effect.from.identifier.id, Effect.Read);
373 operandEffects.set(effect.into.identifier.id, Effect.Store);
374 }
375 break;
376 }
377 case 'CreateFunction':
378 case 'Create': {
379 break;
380 }
381 case 'Mutate': {
382 operandEffects.set(effect.value.identifier.id, Effect.Store);
383 break;
384 }
385 case 'Apply': {
386 CompilerError.invariant(false, {
387 reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
388 loc: effect.function.loc,
389 });
390 }
391 case 'MutateTransitive':
392 case 'MutateConditionally':
393 case 'MutateTransitiveConditionally': {
394 operandEffects.set(
395 effect.value.identifier.id,
396 Effect.ConditionallyMutate,
397 );
398 break;
399 }
400 case 'Freeze': {
401 operandEffects.set(effect.value.identifier.id, Effect.Freeze);
402 break;
403 }
404 case 'ImmutableCapture': {
405 // no-op, Read is the default
406 break;
407 }
408 case 'Impure':
409 case 'Render':
410 case 'MutateFrozen':
411 case 'MutateGlobal': {
412 // no-op
413 break;
414 }
415 default: {
416 assertExhaustive(
417 effect,
418 `Unexpected effect kind ${(effect as any).kind}`,
419 );
420 }
421 }
422 }
423 for (const lvalue of eachInstructionLValue(instr)) {
424 const effect =
425 operandEffects.get(lvalue.identifier.id) ??
426 Effect.ConditionallyMutate;
427 lvalue.effect = effect;
428 }
429 for (const operand of eachInstructionValueOperand(instr.value)) {
430 if (
431 operand.identifier.mutableRange.end > instr.id &&
432 operand.identifier.mutableRange.start === 0
433 ) {
434 operand.identifier.mutableRange.start = instr.id;
435 }
436 const effect = operandEffects.get(operand.identifier.id) ?? Effect.Read;
437 operand.effect = effect;
438 }
439
440 /**
441 * This case is targeted at hoisted functions like:
442 *
443 * ```
444 * x();
445 * function x() { ... }
446 * ```
447 *
448 * Which turns into:
449 *
450 * t0 = DeclareContext HoistedFunction x
451 * t1 = LoadContext x
452 * t2 = CallExpression t1 ( )
453 * t3 = FunctionExpression ...
454 * t4 = StoreContext Function x = t3
455 *
456 * If the function had captured mutable values, it would already have its
457 * range extended to include the StoreContext. But if the function doesn't
458 * capture any mutable values its range won't have been extended yet. We
459 * want to ensure that the value is memoized along with the context variable,
460 * not independently of it (bc of the way we do codegen for hoisted functions).
461 * So here we check for StoreContext rvalues and if they haven't already had
462 * their range extended to at least this instruction, we extend it.
463 */
464 if (
465 instr.value.kind === 'StoreContext' &&
466 instr.value.value.identifier.mutableRange.end <= instr.id
467 ) {
468 instr.value.value.identifier.mutableRange.end = makeInstructionId(
469 instr.id + 1,
470 );
471 }
472 }
473 if (block.terminal.kind === 'return') {
474 block.terminal.value.effect = isFunctionExpression
475 ? Effect.Read
476 : Effect.Freeze;
477 } else {
478 for (const operand of eachTerminalOperand(block.terminal)) {
479 operand.effect = Effect.Read;
480 }
481 }
482 }
483
484 /**
485 * Part 3
486 * Finish populating the externally visible effects. Above we bubble-up the side effects
487 * (MutateFrozen/MutableGlobal/Impure/Render) as well as mutations of context variables.
488 * Here we populate an effect to create the return value as well as populating alias/capture
489 * effects for how data flows between the params, context vars, and return.
490 */
491 const returns = fn.returns.identifier;
492 functionEffects.push({
493 kind: 'Create',
494 into: fn.returns,
495 value: isPrimitiveType(returns)
496 ? ValueKind.Primitive
497 : isJsxType(returns.type)
498 ? ValueKind.Frozen
499 : ValueKind.Mutable,
500 reason: ValueReason.KnownReturnSignature,
501 });
502 /**
503 * Determine precise data-flow effects by simulating transitive mutations of the params/
504 * captures and seeing what other params/context variables are affected. Anything that
505 * would be transitively mutated needs a capture relationship.
506 */
507 const tracked: Array<Place> = [];
508 for (const param of [...fn.params, ...fn.context, fn.returns]) {
509 const place = param.kind === 'Identifier' ? param : param.place;
510 tracked.push(place);
511 }
512 for (const into of tracked) {
513 const mutationIndex = index++;
514 state.mutate(
515 mutationIndex,
516 into.identifier,
517 null,
518 true,
519 MutationKind.Conditional,
520 into.loc,
521 null,
522 null,
523 );
524 for (const from of tracked) {
525 if (
526 from.identifier.id === into.identifier.id ||
527 from.identifier.id === fn.returns.identifier.id
528 ) {
529 continue;
530 }
531 const fromNode = state.nodes.get(from.identifier);
532 CompilerError.invariant(fromNode != null, {
533 reason: `Expected a node to exist for all parameters and context variables`,
534 loc: into.loc,
535 });
536 if (fromNode.lastMutated === mutationIndex) {
537 if (into.identifier.id === fn.returns.identifier.id) {
538 // The return value could be any of the params/context variables
539 functionEffects.push({
540 kind: 'Alias',
541 from,
542 into,
543 });
544 } else {
545 // Otherwise params/context-vars can only capture each other
546 functionEffects.push({
547 kind: 'Capture',
548 from,
549 into,
550 });
551 }
552 }
553 }
554 }
555
556 return functionEffects;
557 }
558
559 function appendFunctionErrors(env: Environment | null, fn: HIRFunction): void {
560 if (env == null) return;
561 for (const effect of fn.aliasingEffects ?? []) {
562 switch (effect.kind) {
563 case 'Impure':
564 case 'MutateFrozen':
565 case 'MutateGlobal': {
566 env.recordError(effect.error);
567 break;
568 }
569 }
570 }
571 }
572
573 export enum MutationKind {
574 None = 0,
575 Conditional = 1,
576 Definite = 2,
577 }
578
579 type Node = {
580 id: Identifier;
581 createdFrom: Map<Identifier, number>;
582 captures: Map<Identifier, number>;
583 aliases: Map<Identifier, number>;
584 maybeAliases: Map<Identifier, number>;
585 edges: Array<{
586 index: number;
587 node: Identifier;
588 kind: 'capture' | 'alias' | 'maybeAlias';
589 }>;
590 transitive: {kind: MutationKind; loc: SourceLocation} | null;
591 local: {kind: MutationKind; loc: SourceLocation} | null;
592 lastMutated: number;
593 mutationReason: MutationReason | null;
594 value:
595 | {kind: 'Object'}
596 | {kind: 'Phi'}
597 | {kind: 'Function'; function: HIRFunction};
598 };
599 class AliasingState {
600 nodes: Map<Identifier, Node> = new Map();
601
602 create(place: Place, value: Node['value']): void {
603 this.nodes.set(place.identifier, {
604 id: place.identifier,
605 createdFrom: new Map(),
606 captures: new Map(),
607 aliases: new Map(),
608 maybeAliases: new Map(),
609 edges: [],
610 transitive: null,
611 local: null,
612 lastMutated: 0,
613 mutationReason: null,
614 value,
615 });
616 }
617
618 createFrom(index: number, from: Place, into: Place): void {
619 this.create(into, {kind: 'Object'});
620 const fromNode = this.nodes.get(from.identifier);
621 const toNode = this.nodes.get(into.identifier);
622 if (fromNode == null || toNode == null) {
623 return;
624 }
625 fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
626 if (!toNode.createdFrom.has(from.identifier)) {
627 toNode.createdFrom.set(from.identifier, index);
628 }
629 }
630
631 capture(index: number, from: Place, into: Place): void {
632 const fromNode = this.nodes.get(from.identifier);
633 const toNode = this.nodes.get(into.identifier);
634 if (fromNode == null || toNode == null) {
635 return;
636 }
637 fromNode.edges.push({index, node: into.identifier, kind: 'capture'});
638 if (!toNode.captures.has(from.identifier)) {
639 toNode.captures.set(from.identifier, index);
640 }
641 }
642
643 assign(index: number, from: Place, into: Place): void {
644 const fromNode = this.nodes.get(from.identifier);
645 const toNode = this.nodes.get(into.identifier);
646 if (fromNode == null || toNode == null) {
647 return;
648 }
649 fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
650 if (!toNode.aliases.has(from.identifier)) {
651 toNode.aliases.set(from.identifier, index);
652 }
653 }
654
655 maybeAlias(index: number, from: Place, into: Place): void {
656 const fromNode = this.nodes.get(from.identifier);
657 const toNode = this.nodes.get(into.identifier);
658 if (fromNode == null || toNode == null) {
659 return;
660 }
661 fromNode.edges.push({index, node: into.identifier, kind: 'maybeAlias'});
662 if (!toNode.maybeAliases.has(from.identifier)) {
663 toNode.maybeAliases.set(from.identifier, index);
664 }
665 }
666
667 render(index: number, start: Identifier, env: Environment | null): void {
668 const seen = new Set<Identifier>();
669 const queue: Array<Identifier> = [start];
670 while (queue.length !== 0) {
671 const current = queue.pop()!;
672 if (seen.has(current)) {
673 continue;
674 }
675 seen.add(current);
676 const node = this.nodes.get(current);
677 if (node == null || node.transitive != null || node.local != null) {
678 continue;
679 }
680 if (node.value.kind === 'Function') {
681 appendFunctionErrors(env, node.value.function);
682 }
683 for (const [alias, when] of node.createdFrom) {
684 if (when >= index) {
685 continue;
686 }
687 queue.push(alias);
688 }
689 for (const [alias, when] of node.aliases) {
690 if (when >= index) {
691 continue;
692 }
693 queue.push(alias);
694 }
695 for (const [capture, when] of node.captures) {
696 if (when >= index) {
697 continue;
698 }
699 queue.push(capture);
700 }
701 }
702 }
703
704 mutate(
705 index: number,
706 start: Identifier,
707 // Null is used for simulated mutations
708 end: InstructionId | null,
709 transitive: boolean,
710 startKind: MutationKind,
711 loc: SourceLocation,
712 reason: MutationReason | null,
713 env: Environment | null,
714 ): void {
715 const seen = new Map<Identifier, MutationKind>();
716 const queue: Array<{
717 place: Identifier;
718 transitive: boolean;
719 direction: 'backwards' | 'forwards';
720 kind: MutationKind;
721 }> = [{place: start, transitive, direction: 'backwards', kind: startKind}];
722 while (queue.length !== 0) {
723 const {place: current, transitive, direction, kind} = queue.pop()!;
724 const previousKind = seen.get(current);
725 if (previousKind != null && previousKind >= kind) {
726 continue;
727 }
728 seen.set(current, kind);
729 const node = this.nodes.get(current);
730 if (node == null) {
731 continue;
732 }
733 node.mutationReason ??= reason;
734 node.lastMutated = Math.max(node.lastMutated, index);
735 if (end != null) {
736 node.id.mutableRange.end = makeInstructionId(
737 Math.max(node.id.mutableRange.end, end),
738 );
739 }
740 if (
741 node.value.kind === 'Function' &&
742 node.transitive == null &&
743 node.local == null
744 ) {
745 appendFunctionErrors(env, node.value.function);
746 }
747 if (transitive) {
748 if (node.transitive == null || node.transitive.kind < kind) {
749 node.transitive = {kind, loc};
750 }
751 } else {
752 if (node.local == null || node.local.kind < kind) {
753 node.local = {kind, loc};
754 }
755 }
756 /**
757 * all mutations affect "forward" edges by the rules:
758 * - Capture a -> b, mutate(a) => mutate(b)
759 * - Alias a -> b, mutate(a) => mutate(b)
760 */
761 for (const edge of node.edges) {
762 if (edge.index >= index) {
763 break;
764 }
765 queue.push({
766 place: edge.node,
767 transitive,
768 direction: 'forwards',
769 // Traversing a maybeAlias edge always downgrades to conditional mutation
770 kind: edge.kind === 'maybeAlias' ? MutationKind.Conditional : kind,
771 });
772 }
773 for (const [alias, when] of node.createdFrom) {
774 if (when >= index) {
775 continue;
776 }
777 queue.push({
778 place: alias,
779 transitive: true,
780 direction: 'backwards',
781 kind,
782 });
783 }
784 if (direction === 'backwards' || node.value.kind !== 'Phi') {
785 /**
786 * all mutations affect backward alias edges by the rules:
787 * - Alias a -> b, mutate(b) => mutate(a)
788 * - Alias a -> b, mutateTransitive(b) => mutate(a)
789 *
790 * However, if we reached a phi because one of its inputs was mutated
791 * (and we're advancing "forwards" through that node's edges), then
792 * we know we've already processed the mutation at its source. The
793 * phi's other inputs can't be affected.
794 */
795 for (const [alias, when] of node.aliases) {
796 if (when >= index) {
797 continue;
798 }
799 queue.push({
800 place: alias,
801 transitive,
802 direction: 'backwards',
803 kind,
804 });
805 }
806 /**
807 * MaybeAlias indicates potential data flow from unknown function calls,
808 * so we downgrade mutations through these aliases to consider them
809 * conditional. This means we'll consider them for mutation *range*
810 * purposes but not report validation errors for mutations, since
811 * we aren't sure that the `from` value could actually be aliased.
812 */
813 for (const [alias, when] of node.maybeAliases) {
814 if (when >= index) {
815 continue;
816 }
817 queue.push({
818 place: alias,
819 transitive,
820 direction: 'backwards',
821 kind: MutationKind.Conditional,
822 });
823 }
824 }
825 /**
826 * but only transitive mutations affect captures
827 */
828 if (transitive) {
829 for (const [capture, when] of node.captures) {
830 if (when >= index) {
831 continue;
832 }
833 queue.push({
834 place: capture,
835 transitive,
836 direction: 'backwards',
837 kind,
838 });
839 }
840 }
841 }
842 }
843 }