main
ts 2,975 lines 95.6 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 {
9 CompilerDiagnostic,
10 CompilerError,
11 Effect,
12 SourceLocation,
13 ValueKind,
14 } from '..';
15 import {
16 BasicBlock,
17 BlockId,
18 DeclarationId,
19 Environment,
20 FunctionExpression,
21 GeneratedSource,
22 getHookKind,
23 HIRFunction,
24 Hole,
25 IdentifierId,
26 Instruction,
27 InstructionKind,
28 InstructionValue,
29 isArrayType,
30 isJsxType,
31 isMapType,
32 isPrimitiveType,
33 isRefOrRefValue,
34 isSetType,
35 makeIdentifierId,
36 Phi,
37 Place,
38 SpreadPattern,
39 Type,
40 ValueReason,
41 } from '../HIR';
42 import {
43 eachInstructionValueOperand,
44 eachPatternItem,
45 eachTerminalOperand,
46 eachTerminalSuccessor,
47 } from '../HIR/visitors';
48
49 import {
50 assertExhaustive,
51 getOrInsertDefault,
52 getOrInsertWith,
53 Set_isSuperset,
54 } from '../Utils/utils';
55 import {
56 printAliasingEffect,
57 printAliasingSignature,
58 printIdentifier,
59 printInstruction,
60 printInstructionValue,
61 printPlace,
62 } from '../HIR/PrintHIR';
63 import {FunctionSignature} from '../HIR/ObjectShape';
64 import prettyFormat from 'pretty-format';
65 import {createTemporaryPlace} from '../HIR/HIRBuilder';
66 import {
67 AliasingEffect,
68 AliasingSignature,
69 hashEffect,
70 MutationReason,
71 } from './AliasingEffects';
72 import {ErrorCategory} from '../CompilerError';
73
74 const DEBUG = false;
75
76 /**
77 * Infers the mutation/aliasing effects for instructions and terminals and annotates
78 * them on the HIR, making the effects of builtin instructions/functions as well as
79 * user-defined functions explicit. These effects then form the basis for subsequent
80 * analysis to determine the mutable range of each value in the program — the set of
81 * instructions over which the value is created and mutated — as well as validation
82 * against invalid code.
83 *
84 * At a high level the approach is:
85 * - Determine a set of candidate effects based purely on the syntax of the instruction
86 * and the types involved. These candidate effects are cached the first time each
87 * instruction is visited. The idea is to reason about the semantics of the instruction
88 * or function in isolation, separately from how those effects may interact with later
89 * abstract interpretation.
90 * - Then we do abstract interpretation over the HIR, iterating until reaching a fixpoint.
91 * This phase tracks the abstract kind of each value (mutable, primitive, frozen, etc)
92 * and the set of values pointed to by each identifier. Each candidate effect is "applied"
93 * to the current abtract state, and effects may be dropped or rewritten accordingly.
94 * For example, a "MutateConditionally <x>" effect may be dropped if x is not a mutable
95 * value. A "Mutate <y>" effect may get converted into a "MutateFrozen <error>" effect
96 * if y is mutable, etc.
97 */
98 export function inferMutationAliasingEffects(
99 fn: HIRFunction,
100 {isFunctionExpression}: {isFunctionExpression: boolean} = {
101 isFunctionExpression: false,
102 },
103 ): void {
104 const initialState = InferenceState.empty(fn.env, isFunctionExpression);
105
106 // Map of blocks to the last (merged) incoming state that was processed
107 const statesByBlock: Map<BlockId, InferenceState> = new Map();
108
109 for (const ref of fn.context) {
110 // TODO: using InstructionValue as a bit of a hack, but it's pragmatic
111 const value: InstructionValue = {
112 kind: 'ObjectExpression',
113 properties: [],
114 loc: ref.loc,
115 };
116 initialState.initialize(value, {
117 kind: ValueKind.Context,
118 reason: new Set([ValueReason.Other]),
119 });
120 initialState.define(ref, value);
121 }
122
123 const paramKind: AbstractValue = isFunctionExpression
124 ? {
125 kind: ValueKind.Mutable,
126 reason: new Set([ValueReason.Other]),
127 }
128 : {
129 kind: ValueKind.Frozen,
130 reason: new Set([ValueReason.ReactiveFunctionArgument]),
131 };
132
133 if (fn.fnType === 'Component') {
134 CompilerError.invariant(fn.params.length <= 2, {
135 reason:
136 'Expected React component to have not more than two parameters: one for props and for ref',
137 loc: fn.loc,
138 });
139 const [props, ref] = fn.params;
140 if (props != null) {
141 inferParam(props, initialState, paramKind);
142 }
143 if (ref != null) {
144 const place = ref.kind === 'Identifier' ? ref : ref.place;
145 const value: InstructionValue = {
146 kind: 'ObjectExpression',
147 properties: [],
148 loc: place.loc,
149 };
150 initialState.initialize(value, {
151 kind: ValueKind.Mutable,
152 reason: new Set([ValueReason.Other]),
153 });
154 initialState.define(place, value);
155 }
156 } else {
157 for (const param of fn.params) {
158 inferParam(param, initialState, paramKind);
159 }
160 }
161
162 /*
163 * Multiple predecessors may be visited prior to reaching a given successor,
164 * so track the list of incoming state for each successor block.
165 * These are merged when reaching that block again.
166 */
167 const queuedStates: Map<BlockId, InferenceState> = new Map();
168 function queue(blockId: BlockId, state: InferenceState): void {
169 let queuedState = queuedStates.get(blockId);
170 if (queuedState != null) {
171 // merge the queued states for this block
172 state = queuedState.merge(state) ?? queuedState;
173 queuedStates.set(blockId, state);
174 } else {
175 /*
176 * this is the first queued state for this block, see whether
177 * there are changed relative to the last time it was processed.
178 */
179 const prevState = statesByBlock.get(blockId);
180 const nextState = prevState != null ? prevState.merge(state) : state;
181 if (nextState != null) {
182 queuedStates.set(blockId, nextState);
183 }
184 }
185 }
186 queue(fn.body.entry, initialState);
187
188 const hoistedContextDeclarations = findHoistedContextDeclarations(fn);
189
190 const context = new Context(
191 isFunctionExpression,
192 fn,
193 hoistedContextDeclarations,
194 findNonMutatedDestructureSpreads(fn),
195 );
196
197 let iterationCount = 0;
198 while (queuedStates.size !== 0) {
199 iterationCount++;
200 if (iterationCount > 100) {
201 CompilerError.invariant(false, {
202 reason: `[InferMutationAliasingEffects] Potential infinite loop`,
203 description: `A value, temporary place, or effect was not cached properly`,
204 loc: fn.loc,
205 });
206 }
207 for (const [blockId, block] of fn.body.blocks) {
208 const incomingState = queuedStates.get(blockId);
209 queuedStates.delete(blockId);
210 if (incomingState == null) {
211 continue;
212 }
213
214 statesByBlock.set(blockId, incomingState);
215 const state = incomingState.clone();
216 inferBlock(context, state, block);
217
218 for (const nextBlockId of eachTerminalSuccessor(block.terminal)) {
219 queue(nextBlockId, state);
220 }
221 }
222 }
223 return;
224 }
225
226 function findHoistedContextDeclarations(
227 fn: HIRFunction,
228 ): Map<DeclarationId, Place | null> {
229 const hoisted = new Map<DeclarationId, Place | null>();
230 function visit(place: Place): void {
231 if (
232 hoisted.has(place.identifier.declarationId) &&
233 hoisted.get(place.identifier.declarationId) == null
234 ) {
235 // If this is the first load of the value, store the location
236 hoisted.set(place.identifier.declarationId, place);
237 }
238 }
239 for (const block of fn.body.blocks.values()) {
240 for (const instr of block.instructions) {
241 if (instr.value.kind === 'DeclareContext') {
242 const kind = instr.value.lvalue.kind;
243 if (
244 kind == InstructionKind.HoistedConst ||
245 kind == InstructionKind.HoistedFunction ||
246 kind == InstructionKind.HoistedLet
247 ) {
248 hoisted.set(instr.value.lvalue.place.identifier.declarationId, null);
249 }
250 } else {
251 for (const operand of eachInstructionValueOperand(instr.value)) {
252 visit(operand);
253 }
254 }
255 }
256 for (const operand of eachTerminalOperand(block.terminal)) {
257 visit(operand);
258 }
259 }
260 return hoisted;
261 }
262
263 class Context {
264 internedEffects: Map<string, AliasingEffect> = new Map();
265 instructionSignatureCache: Map<Instruction, InstructionSignature> = new Map();
266 effectInstructionValueCache: Map<AliasingEffect, InstructionValue> =
267 new Map();
268 applySignatureCache: Map<
269 AliasingSignature,
270 Map<AliasingEffect, Array<AliasingEffect> | null>
271 > = new Map();
272 catchHandlers: Map<BlockId, Place> = new Map();
273 functionSignatureCache: Map<FunctionExpression, AliasingSignature> =
274 new Map();
275 isFuctionExpression: boolean;
276 fn: HIRFunction;
277 hoistedContextDeclarations: Map<DeclarationId, Place | null>;
278 nonMutatingSpreads: Set<IdentifierId>;
279
280 constructor(
281 isFunctionExpression: boolean,
282 fn: HIRFunction,
283 hoistedContextDeclarations: Map<DeclarationId, Place | null>,
284 nonMutatingSpreads: Set<IdentifierId>,
285 ) {
286 this.isFuctionExpression = isFunctionExpression;
287 this.fn = fn;
288 this.hoistedContextDeclarations = hoistedContextDeclarations;
289 this.nonMutatingSpreads = nonMutatingSpreads;
290 }
291
292 cacheApplySignature(
293 signature: AliasingSignature,
294 effect: Extract<AliasingEffect, {kind: 'Apply'}>,
295 f: () => Array<AliasingEffect> | null,
296 ): Array<AliasingEffect> | null {
297 const inner = getOrInsertDefault(
298 this.applySignatureCache,
299 signature,
300 new Map(),
301 );
302 return getOrInsertWith(inner, effect, f);
303 }
304
305 internEffect(effect: AliasingEffect): AliasingEffect {
306 const hash = hashEffect(effect);
307 let interned = this.internedEffects.get(hash);
308 if (interned == null) {
309 this.internedEffects.set(hash, effect);
310 interned = effect;
311 }
312 return interned;
313 }
314 }
315
316 /**
317 * Finds objects created via ObjectPattern spread destructuring
318 * (`const {x, ...spread} = ...`) where a) the rvalue is known frozen and
319 * b) the spread value cannot possibly be directly mutated. The idea is that
320 * for this set of values, we can treat the spread object as frozen.
321 *
322 * The primary use case for this is props spreading:
323 *
324 * ```
325 * function Component({prop, ...otherProps}) {
326 * const transformedProp = transform(prop, otherProps.foo);
327 * // pass `otherProps` down:
328 * return <Foo {...otherProps} prop={transformedProp} />;
329 * }
330 * ```
331 *
332 * Here we know that since `otherProps` cannot be mutated, we don't have to treat
333 * it as mutable: `otherProps.foo` only reads a value that must be frozen, so it
334 * can be treated as frozen too.
335 */
336 function findNonMutatedDestructureSpreads(fn: HIRFunction): Set<IdentifierId> {
337 const knownFrozen = new Set<IdentifierId>();
338 if (fn.fnType === 'Component') {
339 const [props] = fn.params;
340 if (props != null && props.kind === 'Identifier') {
341 knownFrozen.add(props.identifier.id);
342 }
343 } else {
344 for (const param of fn.params) {
345 if (param.kind === 'Identifier') {
346 knownFrozen.add(param.identifier.id);
347 }
348 }
349 }
350
351 // Map of temporaries to identifiers for spread objects
352 const candidateNonMutatingSpreads = new Map<IdentifierId, IdentifierId>();
353 for (const block of fn.body.blocks.values()) {
354 if (candidateNonMutatingSpreads.size !== 0) {
355 for (const phi of block.phis) {
356 for (const operand of phi.operands.values()) {
357 const spread = candidateNonMutatingSpreads.get(operand.identifier.id);
358 if (spread != null) {
359 candidateNonMutatingSpreads.delete(spread);
360 }
361 }
362 }
363 }
364 for (const instr of block.instructions) {
365 const {lvalue, value} = instr;
366 switch (value.kind) {
367 case 'Destructure': {
368 if (
369 !knownFrozen.has(value.value.identifier.id) ||
370 !(
371 value.lvalue.kind === InstructionKind.Let ||
372 value.lvalue.kind === InstructionKind.Const
373 ) ||
374 value.lvalue.pattern.kind !== 'ObjectPattern'
375 ) {
376 continue;
377 }
378 for (const item of value.lvalue.pattern.properties) {
379 if (item.kind !== 'Spread') {
380 continue;
381 }
382 candidateNonMutatingSpreads.set(
383 item.place.identifier.id,
384 item.place.identifier.id,
385 );
386 }
387 break;
388 }
389 case 'LoadLocal': {
390 const spread = candidateNonMutatingSpreads.get(
391 value.place.identifier.id,
392 );
393 if (spread != null) {
394 candidateNonMutatingSpreads.set(lvalue.identifier.id, spread);
395 }
396 break;
397 }
398 case 'StoreLocal': {
399 const spread = candidateNonMutatingSpreads.get(
400 value.value.identifier.id,
401 );
402 if (spread != null) {
403 candidateNonMutatingSpreads.set(lvalue.identifier.id, spread);
404 candidateNonMutatingSpreads.set(
405 value.lvalue.place.identifier.id,
406 spread,
407 );
408 }
409 break;
410 }
411 case 'JsxFragment':
412 case 'JsxExpression': {
413 // Passing objects created with spread to jsx can't mutate them
414 break;
415 }
416 case 'PropertyLoad': {
417 // Properties must be frozen since the original value was frozen
418 break;
419 }
420 case 'CallExpression':
421 case 'MethodCall': {
422 const callee =
423 value.kind === 'CallExpression' ? value.callee : value.property;
424 if (getHookKind(fn.env, callee.identifier) != null) {
425 // Hook calls have frozen arguments, and non-ref returns are frozen
426 if (!isRefOrRefValue(lvalue.identifier)) {
427 knownFrozen.add(lvalue.identifier.id);
428 }
429 } else {
430 // Non-hook calls check their operands, since they are potentially mutable
431 if (candidateNonMutatingSpreads.size !== 0) {
432 // Otherwise any reference to the spread object itself may mutate
433 for (const operand of eachInstructionValueOperand(value)) {
434 const spread = candidateNonMutatingSpreads.get(
435 operand.identifier.id,
436 );
437 if (spread != null) {
438 candidateNonMutatingSpreads.delete(spread);
439 }
440 }
441 }
442 }
443 break;
444 }
445 default: {
446 if (candidateNonMutatingSpreads.size !== 0) {
447 // Otherwise any reference to the spread object itself may mutate
448 for (const operand of eachInstructionValueOperand(value)) {
449 const spread = candidateNonMutatingSpreads.get(
450 operand.identifier.id,
451 );
452 if (spread != null) {
453 candidateNonMutatingSpreads.delete(spread);
454 }
455 }
456 }
457 }
458 }
459 }
460 }
461
462 const nonMutatingSpreads = new Set<IdentifierId>();
463 for (const [key, value] of candidateNonMutatingSpreads) {
464 if (key === value) {
465 nonMutatingSpreads.add(key);
466 }
467 }
468 return nonMutatingSpreads;
469 }
470
471 function inferParam(
472 param: Place | SpreadPattern,
473 initialState: InferenceState,
474 paramKind: AbstractValue,
475 ): void {
476 const place = param.kind === 'Identifier' ? param : param.place;
477 const value: InstructionValue = {
478 kind: 'Primitive',
479 loc: place.loc,
480 value: undefined,
481 };
482 initialState.initialize(value, paramKind);
483 initialState.define(place, value);
484 }
485
486 function inferBlock(
487 context: Context,
488 state: InferenceState,
489 block: BasicBlock,
490 ): void {
491 for (const phi of block.phis) {
492 state.inferPhi(phi);
493 }
494
495 for (const instr of block.instructions) {
496 let instructionSignature = context.instructionSignatureCache.get(instr);
497 if (instructionSignature == null) {
498 instructionSignature = computeSignatureForInstruction(
499 context,
500 state.env,
501 instr,
502 );
503 context.instructionSignatureCache.set(instr, instructionSignature);
504 }
505 const effects = applySignature(context, state, instructionSignature, instr);
506 instr.effects = effects;
507 }
508 const terminal = block.terminal;
509 if (terminal.kind === 'try' && terminal.handlerBinding != null) {
510 context.catchHandlers.set(terminal.handler, terminal.handlerBinding);
511 } else if (terminal.kind === 'maybe-throw' && terminal.handler !== null) {
512 const handlerParam = context.catchHandlers.get(terminal.handler);
513 if (handlerParam != null) {
514 CompilerError.invariant(state.kind(handlerParam) != null, {
515 reason:
516 'Expected catch binding to be initialized with a DeclareLocal Catch instruction',
517 loc: terminal.loc,
518 });
519 const effects: Array<AliasingEffect> = [];
520 for (const instr of block.instructions) {
521 if (
522 instr.value.kind === 'CallExpression' ||
523 instr.value.kind === 'MethodCall'
524 ) {
525 /**
526 * Many instructions can error, but only calls can throw their result as the error
527 * itself. For example, `c = a.b` can throw if `a` is nullish, but the thrown value
528 * is an error object synthesized by the JS runtime. Whereas `throwsInput(x)` can
529 * throw (effectively) the result of the call.
530 *
531 * TODO: call applyEffect() instead. This meant that the catch param wasn't inferred
532 * as a mutable value, though. See `try-catch-try-value-modified-in-catch-escaping.js`
533 * fixture as an example
534 */
535 state.appendAlias(handlerParam, instr.lvalue);
536 const kind = state.kind(instr.lvalue).kind;
537 if (kind === ValueKind.Mutable || kind == ValueKind.Context) {
538 effects.push(
539 context.internEffect({
540 kind: 'Alias',
541 from: instr.lvalue,
542 into: handlerParam,
543 }),
544 );
545 }
546 }
547 }
548 terminal.effects = effects.length !== 0 ? effects : null;
549 }
550 } else if (terminal.kind === 'return') {
551 if (!context.isFuctionExpression) {
552 terminal.effects = [
553 context.internEffect({
554 kind: 'Freeze',
555 value: terminal.value,
556 reason: ValueReason.JsxCaptured,
557 }),
558 ];
559 }
560 }
561 }
562
563 /**
564 * Applies the signature to the given state to determine the precise set of effects
565 * that will occur in practice. This takes into account the inferred state of each
566 * variable. For example, the signature may have a `ConditionallyMutate x` effect.
567 * Here, we check the abstract type of `x` and either record a `Mutate x` if x is mutable
568 * or no effect if x is a primitive, global, or frozen.
569 *
570 * This phase may also emit errors, for example MutateLocal on a frozen value is invalid.
571 */
572 function applySignature(
573 context: Context,
574 state: InferenceState,
575 signature: InstructionSignature,
576 instruction: Instruction,
577 ): Array<AliasingEffect> | null {
578 const effects: Array<AliasingEffect> = [];
579 /**
580 * For function instructions, eagerly validate that they aren't mutating
581 * a known-frozen value.
582 *
583 * TODO: make sure we're also validating against global mutations somewhere, but
584 * account for this being allowed in effects/event handlers.
585 */
586 if (
587 instruction.value.kind === 'FunctionExpression' ||
588 instruction.value.kind === 'ObjectMethod'
589 ) {
590 const aliasingEffects =
591 instruction.value.loweredFunc.func.aliasingEffects ?? [];
592 const context = new Set(
593 instruction.value.loweredFunc.func.context.map(p => p.identifier.id),
594 );
595 for (const effect of aliasingEffects) {
596 if (effect.kind === 'Mutate' || effect.kind === 'MutateTransitive') {
597 if (!context.has(effect.value.identifier.id)) {
598 continue;
599 }
600 const value = state.kind(effect.value);
601 switch (value.kind) {
602 case ValueKind.Frozen: {
603 const reason = getWriteErrorReason({
604 kind: value.kind,
605 reason: value.reason,
606 });
607 const variable =
608 effect.value.identifier.name !== null &&
609 effect.value.identifier.name.kind === 'named'
610 ? `\`${effect.value.identifier.name.value}\``
611 : 'value';
612 const diagnostic = CompilerDiagnostic.create({
613 category: ErrorCategory.Immutability,
614 reason: 'This value cannot be modified',
615 description: reason,
616 }).withDetails({
617 kind: 'error',
618 loc: effect.value.loc,
619 message: `${variable} cannot be modified`,
620 });
621 if (
622 effect.kind === 'Mutate' &&
623 effect.reason?.kind === 'AssignCurrentProperty'
624 ) {
625 diagnostic.withDetails({
626 kind: 'hint',
627 message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
628 });
629 }
630 effects.push({
631 kind: 'MutateFrozen',
632 place: effect.value,
633 error: diagnostic,
634 });
635 }
636 }
637 }
638 }
639 }
640
641 /*
642 * Track which values we've already aliased once, so that we can switch to
643 * appendAlias() for subsequent aliases into the same value
644 */
645 const initialized = new Set<IdentifierId>();
646
647 if (DEBUG) {
648 console.log(printInstruction(instruction));
649 }
650
651 for (const effect of signature.effects) {
652 applyEffect(context, state, effect, initialized, effects);
653 }
654 if (DEBUG) {
655 console.log(
656 prettyFormat(state.debugAbstractValue(state.kind(instruction.lvalue))),
657 );
658 console.log(
659 effects.map(effect => ` ${printAliasingEffect(effect)}`).join('\n'),
660 );
661 }
662 if (
663 !(state.isDefined(instruction.lvalue) && state.kind(instruction.lvalue))
664 ) {
665 CompilerError.invariant(false, {
666 reason: `Expected instruction lvalue to be initialized`,
667 loc: instruction.loc,
668 });
669 }
670 return effects.length !== 0 ? effects : null;
671 }
672
673 function applyEffect(
674 context: Context,
675 state: InferenceState,
676 _effect: AliasingEffect,
677 initialized: Set<IdentifierId>,
678 effects: Array<AliasingEffect>,
679 ): void {
680 const effect = context.internEffect(_effect);
681 if (DEBUG) {
682 console.log(printAliasingEffect(effect));
683 }
684 switch (effect.kind) {
685 case 'Freeze': {
686 const didFreeze = state.freeze(effect.value, effect.reason);
687 if (didFreeze) {
688 effects.push(effect);
689 }
690 break;
691 }
692 case 'Create': {
693 CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
694 reason: `Cannot re-initialize variable within an instruction`,
695 description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
696 loc: effect.into.loc,
697 });
698 initialized.add(effect.into.identifier.id);
699
700 let value = context.effectInstructionValueCache.get(effect);
701 if (value == null) {
702 value = {
703 kind: 'ObjectExpression',
704 properties: [],
705 loc: effect.into.loc,
706 };
707 context.effectInstructionValueCache.set(effect, value);
708 }
709 state.initialize(value, {
710 kind: effect.value,
711 reason: new Set([effect.reason]),
712 });
713 state.define(effect.into, value);
714 effects.push(effect);
715 break;
716 }
717 case 'ImmutableCapture': {
718 const kind = state.kind(effect.from).kind;
719 switch (kind) {
720 case ValueKind.Global:
721 case ValueKind.Primitive: {
722 // no-op: we don't need to track data flow for copy types
723 break;
724 }
725 default: {
726 effects.push(effect);
727 }
728 }
729 break;
730 }
731 case 'CreateFrom': {
732 CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
733 reason: `Cannot re-initialize variable within an instruction`,
734 description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
735 loc: effect.into.loc,
736 });
737 initialized.add(effect.into.identifier.id);
738
739 const fromValue = state.kind(effect.from);
740 let value = context.effectInstructionValueCache.get(effect);
741 if (value == null) {
742 value = {
743 kind: 'ObjectExpression',
744 properties: [],
745 loc: effect.into.loc,
746 };
747 context.effectInstructionValueCache.set(effect, value);
748 }
749 state.initialize(value, {
750 kind: fromValue.kind,
751 reason: new Set(fromValue.reason),
752 });
753 state.define(effect.into, value);
754 switch (fromValue.kind) {
755 case ValueKind.Primitive:
756 case ValueKind.Global: {
757 effects.push({
758 kind: 'Create',
759 value: fromValue.kind,
760 into: effect.into,
761 reason: [...fromValue.reason][0] ?? ValueReason.Other,
762 });
763 break;
764 }
765 case ValueKind.Frozen: {
766 effects.push({
767 kind: 'Create',
768 value: fromValue.kind,
769 into: effect.into,
770 reason: [...fromValue.reason][0] ?? ValueReason.Other,
771 });
772 applyEffect(
773 context,
774 state,
775 {
776 kind: 'ImmutableCapture',
777 from: effect.from,
778 into: effect.into,
779 },
780 initialized,
781 effects,
782 );
783 break;
784 }
785 default: {
786 effects.push(effect);
787 }
788 }
789 break;
790 }
791 case 'CreateFunction': {
792 CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
793 reason: `Cannot re-initialize variable within an instruction`,
794 description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
795 loc: effect.into.loc,
796 });
797 initialized.add(effect.into.identifier.id);
798
799 effects.push(effect);
800 /**
801 * We consider the function mutable if it has any mutable context variables or
802 * any side-effects that need to be tracked if the function is called.
803 */
804 const hasCaptures = effect.captures.some(capture => {
805 switch (state.kind(capture).kind) {
806 case ValueKind.Context:
807 case ValueKind.Mutable: {
808 return true;
809 }
810 default: {
811 return false;
812 }
813 }
814 });
815 const hasTrackedSideEffects =
816 effect.function.loweredFunc.func.aliasingEffects?.some(
817 effect =>
818 // TODO; include "render" here?
819 effect.kind === 'MutateFrozen' ||
820 effect.kind === 'MutateGlobal' ||
821 effect.kind === 'Impure',
822 );
823 // For legacy compatibility
824 const capturesRef = effect.function.loweredFunc.func.context.some(
825 operand => isRefOrRefValue(operand.identifier),
826 );
827 const isMutable = hasCaptures || hasTrackedSideEffects || capturesRef;
828 for (const operand of effect.function.loweredFunc.func.context) {
829 if (operand.effect !== Effect.Capture) {
830 continue;
831 }
832 const kind = state.kind(operand).kind;
833 if (
834 kind === ValueKind.Primitive ||
835 kind == ValueKind.Frozen ||
836 kind == ValueKind.Global
837 ) {
838 operand.effect = Effect.Read;
839 }
840 }
841 state.initialize(effect.function, {
842 kind: isMutable ? ValueKind.Mutable : ValueKind.Frozen,
843 reason: new Set([]),
844 });
845 state.define(effect.into, effect.function);
846 for (const capture of effect.captures) {
847 applyEffect(
848 context,
849 state,
850 {
851 kind: 'Capture',
852 from: capture,
853 into: effect.into,
854 },
855 initialized,
856 effects,
857 );
858 }
859 break;
860 }
861 case 'MaybeAlias':
862 case 'Alias':
863 case 'Capture': {
864 CompilerError.invariant(
865 effect.kind === 'Capture' ||
866 effect.kind === 'MaybeAlias' ||
867 initialized.has(effect.into.identifier.id),
868 {
869 reason: `Expected destination to already be initialized within this instruction`,
870 description:
871 `Destination ${printPlace(effect.into)} is not initialized in this ` +
872 `instruction for effect ${printAliasingEffect(effect)}`,
873 loc: effect.into.loc,
874 },
875 );
876 /*
877 * Capture describes potential information flow: storing a pointer to one value
878 * within another. If the destination is not mutable, or the source value has
879 * copy-on-write semantics, then we can prune the effect
880 */
881 const intoKind = state.kind(effect.into).kind;
882 let destinationType: 'context' | 'mutable' | null = null;
883 switch (intoKind) {
884 case ValueKind.Context: {
885 destinationType = 'context';
886 break;
887 }
888 case ValueKind.Mutable:
889 case ValueKind.MaybeFrozen: {
890 destinationType = 'mutable';
891 break;
892 }
893 }
894 const fromKind = state.kind(effect.from).kind;
895 let sourceType: 'context' | 'mutable' | 'frozen' | null = null;
896 switch (fromKind) {
897 case ValueKind.Context: {
898 sourceType = 'context';
899 break;
900 }
901 case ValueKind.Global:
902 case ValueKind.Primitive: {
903 break;
904 }
905 case ValueKind.MaybeFrozen:
906 case ValueKind.Frozen: {
907 sourceType = 'frozen';
908 break;
909 }
910 default: {
911 sourceType = 'mutable';
912 break;
913 }
914 }
915
916 if (sourceType === 'frozen') {
917 applyEffect(
918 context,
919 state,
920 {
921 kind: 'ImmutableCapture',
922 from: effect.from,
923 into: effect.into,
924 },
925 initialized,
926 effects,
927 );
928 } else if (
929 (sourceType === 'mutable' && destinationType === 'mutable') ||
930 effect.kind === 'MaybeAlias'
931 ) {
932 effects.push(effect);
933 } else if (
934 (sourceType === 'context' && destinationType != null) ||
935 (sourceType === 'mutable' && destinationType === 'context')
936 ) {
937 applyEffect(
938 context,
939 state,
940 {kind: 'MaybeAlias', from: effect.from, into: effect.into},
941 initialized,
942 effects,
943 );
944 }
945 break;
946 }
947 case 'Assign': {
948 CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
949 reason: `Cannot re-initialize variable within an instruction`,
950 description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
951 loc: effect.into.loc,
952 });
953 initialized.add(effect.into.identifier.id);
954
955 /*
956 * Alias represents potential pointer aliasing. If the type is a global,
957 * a primitive (copy-on-write semantics) then we can prune the effect
958 */
959 const fromValue = state.kind(effect.from);
960 const fromKind = fromValue.kind;
961 switch (fromKind) {
962 case ValueKind.Frozen: {
963 applyEffect(
964 context,
965 state,
966 {
967 kind: 'ImmutableCapture',
968 from: effect.from,
969 into: effect.into,
970 },
971 initialized,
972 effects,
973 );
974 let value = context.effectInstructionValueCache.get(effect);
975 if (value == null) {
976 value = {
977 kind: 'Primitive',
978 value: undefined,
979 loc: effect.from.loc,
980 };
981 context.effectInstructionValueCache.set(effect, value);
982 }
983 state.initialize(value, {
984 kind: fromKind,
985 reason: new Set(fromValue.reason),
986 });
987 state.define(effect.into, value);
988 break;
989 }
990 case ValueKind.Global:
991 case ValueKind.Primitive: {
992 let value = context.effectInstructionValueCache.get(effect);
993 if (value == null) {
994 value = {
995 kind: 'Primitive',
996 value: undefined,
997 loc: effect.from.loc,
998 };
999 context.effectInstructionValueCache.set(effect, value);
1000 }
1001 state.initialize(value, {
1002 kind: fromKind,
1003 reason: new Set(fromValue.reason),
1004 });
1005 state.define(effect.into, value);
1006 break;
1007 }
1008 default: {
1009 state.assign(effect.into, effect.from);
1010 effects.push(effect);
1011 break;
1012 }
1013 }
1014 break;
1015 }
1016 case 'Apply': {
1017 const functionValues = state.values(effect.function);
1018 if (
1019 functionValues.length === 1 &&
1020 functionValues[0].kind === 'FunctionExpression' &&
1021 functionValues[0].loweredFunc.func.aliasingEffects != null
1022 ) {
1023 /*
1024 * We're calling a locally declared function, we already know it's effects!
1025 * We just have to substitute in the args for the params
1026 */
1027 const functionExpr = functionValues[0];
1028 let signature = context.functionSignatureCache.get(functionExpr);
1029 if (signature == null) {
1030 signature = buildSignatureFromFunctionExpression(
1031 state.env,
1032 functionExpr,
1033 );
1034 context.functionSignatureCache.set(functionExpr, signature);
1035 }
1036 if (DEBUG) {
1037 console.log(
1038 `constructed alias signature:\n${printAliasingSignature(signature)}`,
1039 );
1040 }
1041 const signatureEffects = context.cacheApplySignature(
1042 signature,
1043 effect,
1044 () =>
1045 computeEffectsForSignature(
1046 state.env,
1047 signature,
1048 effect.into,
1049 effect.receiver,
1050 effect.args,
1051 functionExpr.loweredFunc.func.context,
1052 effect.loc,
1053 ),
1054 );
1055 if (signatureEffects != null) {
1056 applyEffect(
1057 context,
1058 state,
1059 {kind: 'MutateTransitiveConditionally', value: effect.function},
1060 initialized,
1061 effects,
1062 );
1063 for (const signatureEffect of signatureEffects) {
1064 applyEffect(context, state, signatureEffect, initialized, effects);
1065 }
1066 break;
1067 }
1068 }
1069 let signatureEffects = null;
1070 if (effect.signature?.aliasing != null) {
1071 const signature = effect.signature.aliasing;
1072 signatureEffects = context.cacheApplySignature(
1073 effect.signature.aliasing,
1074 effect,
1075 () =>
1076 computeEffectsForSignature(
1077 state.env,
1078 signature,
1079 effect.into,
1080 effect.receiver,
1081 effect.args,
1082 [],
1083 effect.loc,
1084 ),
1085 );
1086 }
1087 if (signatureEffects != null) {
1088 for (const signatureEffect of signatureEffects) {
1089 applyEffect(context, state, signatureEffect, initialized, effects);
1090 }
1091 } else if (effect.signature != null) {
1092 const legacyEffects = computeEffectsForLegacySignature(
1093 state,
1094 effect.signature,
1095 effect.into,
1096 effect.receiver,
1097 effect.args,
1098 effect.loc,
1099 );
1100 for (const legacyEffect of legacyEffects) {
1101 applyEffect(context, state, legacyEffect, initialized, effects);
1102 }
1103 } else {
1104 applyEffect(
1105 context,
1106 state,
1107 {
1108 kind: 'Create',
1109 into: effect.into,
1110 value: ValueKind.Mutable,
1111 reason: ValueReason.Other,
1112 },
1113 initialized,
1114 effects,
1115 );
1116 /*
1117 * If no signature then by default:
1118 * - All operands are conditionally mutated, except some instruction
1119 * variants are assumed to not mutate the callee (such as `new`)
1120 * - All operands are captured into (but not directly aliased as)
1121 * every other argument.
1122 */
1123 for (const arg of [effect.receiver, effect.function, ...effect.args]) {
1124 if (arg.kind === 'Hole') {
1125 continue;
1126 }
1127 const operand = arg.kind === 'Identifier' ? arg : arg.place;
1128 if (operand !== effect.function || effect.mutatesFunction) {
1129 applyEffect(
1130 context,
1131 state,
1132 {
1133 kind: 'MutateTransitiveConditionally',
1134 value: operand,
1135 },
1136 initialized,
1137 effects,
1138 );
1139 }
1140 const mutateIterator =
1141 arg.kind === 'Spread' ? conditionallyMutateIterator(operand) : null;
1142 if (mutateIterator) {
1143 applyEffect(context, state, mutateIterator, initialized, effects);
1144 }
1145 applyEffect(
1146 context,
1147 state,
1148 // OK: recording information flow
1149 {kind: 'MaybeAlias', from: operand, into: effect.into},
1150 initialized,
1151 effects,
1152 );
1153 for (const otherArg of [
1154 effect.receiver,
1155 effect.function,
1156 ...effect.args,
1157 ]) {
1158 if (otherArg.kind === 'Hole') {
1159 continue;
1160 }
1161 const other =
1162 otherArg.kind === 'Identifier' ? otherArg : otherArg.place;
1163 if (other === arg) {
1164 continue;
1165 }
1166 applyEffect(
1167 context,
1168 state,
1169 {
1170 /*
1171 * OK: a function might store one operand into another,
1172 * but it can't force one to alias another
1173 */
1174 kind: 'Capture',
1175 from: operand,
1176 into: other,
1177 },
1178 initialized,
1179 effects,
1180 );
1181 }
1182 }
1183 }
1184 break;
1185 }
1186 case 'Mutate':
1187 case 'MutateConditionally':
1188 case 'MutateTransitive':
1189 case 'MutateTransitiveConditionally': {
1190 const mutationKind = state.mutate(effect.kind, effect.value);
1191 if (mutationKind === 'mutate') {
1192 effects.push(effect);
1193 } else if (mutationKind === 'mutate-ref') {
1194 // no-op
1195 } else if (
1196 mutationKind !== 'none' &&
1197 (effect.kind === 'Mutate' || effect.kind === 'MutateTransitive')
1198 ) {
1199 const value = state.kind(effect.value);
1200 if (DEBUG) {
1201 console.log(`invalid mutation: ${printAliasingEffect(effect)}`);
1202 console.log(prettyFormat(state.debugAbstractValue(value)));
1203 }
1204
1205 if (
1206 mutationKind === 'mutate-frozen' &&
1207 context.hoistedContextDeclarations.has(
1208 effect.value.identifier.declarationId,
1209 )
1210 ) {
1211 const variable =
1212 effect.value.identifier.name !== null &&
1213 effect.value.identifier.name.kind === 'named'
1214 ? `\`${effect.value.identifier.name.value}\``
1215 : null;
1216 const hoistedAccess = context.hoistedContextDeclarations.get(
1217 effect.value.identifier.declarationId,
1218 );
1219 const diagnostic = CompilerDiagnostic.create({
1220 category: ErrorCategory.Immutability,
1221 reason: 'Cannot access variable before it is declared',
1222 description: `${variable ?? 'This variable'} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`,
1223 });
1224 if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) {
1225 diagnostic.withDetails({
1226 kind: 'error',
1227 loc: hoistedAccess.loc,
1228 message: `${variable ?? 'variable'} accessed before it is declared`,
1229 });
1230 }
1231 diagnostic.withDetails({
1232 kind: 'error',
1233 loc: effect.value.loc,
1234 message: `${variable ?? 'variable'} is declared here`,
1235 });
1236
1237 applyEffect(
1238 context,
1239 state,
1240 {
1241 kind: 'MutateFrozen',
1242 place: effect.value,
1243 error: diagnostic,
1244 },
1245 initialized,
1246 effects,
1247 );
1248 } else {
1249 const reason = getWriteErrorReason({
1250 kind: value.kind,
1251 reason: value.reason,
1252 });
1253 const variable =
1254 effect.value.identifier.name !== null &&
1255 effect.value.identifier.name.kind === 'named'
1256 ? `\`${effect.value.identifier.name.value}\``
1257 : 'value';
1258 const diagnostic = CompilerDiagnostic.create({
1259 category: ErrorCategory.Immutability,
1260 reason: 'This value cannot be modified',
1261 description: reason,
1262 }).withDetails({
1263 kind: 'error',
1264 loc: effect.value.loc,
1265 message: `${variable} cannot be modified`,
1266 });
1267 if (
1268 effect.kind === 'Mutate' &&
1269 effect.reason?.kind === 'AssignCurrentProperty'
1270 ) {
1271 diagnostic.withDetails({
1272 kind: 'hint',
1273 message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
1274 });
1275 }
1276 applyEffect(
1277 context,
1278 state,
1279 {
1280 kind:
1281 value.kind === ValueKind.Frozen
1282 ? 'MutateFrozen'
1283 : 'MutateGlobal',
1284 place: effect.value,
1285 error: diagnostic,
1286 },
1287 initialized,
1288 effects,
1289 );
1290 }
1291 }
1292 break;
1293 }
1294 case 'Impure':
1295 case 'Render':
1296 case 'MutateFrozen':
1297 case 'MutateGlobal': {
1298 effects.push(effect);
1299 break;
1300 }
1301 default: {
1302 assertExhaustive(
1303 effect,
1304 `Unexpected effect kind '${(effect as any).kind as any}'`,
1305 );
1306 }
1307 }
1308 }
1309
1310 class InferenceState {
1311 env: Environment;
1312 #isFunctionExpression: boolean;
1313
1314 // The kind of each value, based on its allocation site
1315 #values: Map<InstructionValue, AbstractValue>;
1316 /*
1317 * The set of values pointed to by each identifier. This is a set
1318 * to accommodate phi points (where a variable may have different
1319 * values from different control flow paths).
1320 */
1321 #variables: Map<IdentifierId, Set<InstructionValue>>;
1322
1323 constructor(
1324 env: Environment,
1325 isFunctionExpression: boolean,
1326 values: Map<InstructionValue, AbstractValue>,
1327 variables: Map<IdentifierId, Set<InstructionValue>>,
1328 ) {
1329 this.env = env;
1330 this.#isFunctionExpression = isFunctionExpression;
1331 this.#values = values;
1332 this.#variables = variables;
1333 }
1334
1335 static empty(
1336 env: Environment,
1337 isFunctionExpression: boolean,
1338 ): InferenceState {
1339 return new InferenceState(env, isFunctionExpression, new Map(), new Map());
1340 }
1341
1342 get isFunctionExpression(): boolean {
1343 return this.#isFunctionExpression;
1344 }
1345
1346 // (Re)initializes a @param value with its default @param kind.
1347 initialize(value: InstructionValue, kind: AbstractValue): void {
1348 CompilerError.invariant(value.kind !== 'LoadLocal', {
1349 reason:
1350 '[InferMutationAliasingEffects] Expected all top-level identifiers to be defined as variables, not values',
1351 loc: value.loc,
1352 });
1353 this.#values.set(value, kind);
1354 }
1355
1356 values(place: Place): Array<InstructionValue> {
1357 const values = this.#variables.get(place.identifier.id);
1358 CompilerError.invariant(values != null, {
1359 reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`,
1360 description: `${printPlace(place)}`,
1361 message: 'this is uninitialized',
1362 loc: place.loc,
1363 });
1364 return Array.from(values);
1365 }
1366
1367 // Lookup the kind of the given @param value.
1368 kind(place: Place): AbstractValue {
1369 const values = this.#variables.get(place.identifier.id);
1370 CompilerError.invariant(values != null, {
1371 reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`,
1372 description: `${printPlace(place)}`,
1373 message: 'this is uninitialized',
1374 loc: place.loc,
1375 });
1376 let mergedKind: AbstractValue | null = null;
1377 for (const value of values) {
1378 const kind = this.#values.get(value)!;
1379 mergedKind =
1380 mergedKind !== null ? mergeAbstractValues(mergedKind, kind) : kind;
1381 }
1382 CompilerError.invariant(mergedKind !== null, {
1383 reason: `[InferMutationAliasingEffects] Expected at least one value`,
1384 description: `No value found at \`${printPlace(place)}\``,
1385 loc: place.loc,
1386 });
1387 return mergedKind;
1388 }
1389
1390 // Updates the value at @param place to point to the same value as @param value.
1391 assign(place: Place, value: Place): void {
1392 const values = this.#variables.get(value.identifier.id);
1393 CompilerError.invariant(values != null, {
1394 reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
1395 description: `${printIdentifier(value.identifier)}`,
1396 message: 'Expected value for identifier to be initialized',
1397 loc: value.loc,
1398 });
1399 this.#variables.set(place.identifier.id, new Set(values));
1400 }
1401
1402 appendAlias(place: Place, value: Place): void {
1403 const values = this.#variables.get(value.identifier.id);
1404 CompilerError.invariant(values != null, {
1405 reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
1406 description: `${printIdentifier(value.identifier)}`,
1407 message: 'Expected value for identifier to be initialized',
1408 loc: value.loc,
1409 });
1410 const prevValues = this.values(place);
1411 this.#variables.set(
1412 place.identifier.id,
1413 new Set([...prevValues, ...values]),
1414 );
1415 }
1416
1417 // Defines (initializing or updating) a variable with a specific kind of value.
1418 define(place: Place, value: InstructionValue): void {
1419 CompilerError.invariant(this.#values.has(value), {
1420 reason: `[InferMutationAliasingEffects] Expected value to be initialized`,
1421 description: printInstructionValue(value),
1422 loc: value.loc,
1423 });
1424 this.#variables.set(place.identifier.id, new Set([value]));
1425 }
1426
1427 isDefined(place: Place): boolean {
1428 return this.#variables.has(place.identifier.id);
1429 }
1430
1431 /**
1432 * Marks @param place as transitively frozen. Returns true if the value was not
1433 * already frozen, false if the value is already frozen (or already known immutable).
1434 */
1435 freeze(place: Place, reason: ValueReason): boolean {
1436 const value = this.kind(place);
1437 switch (value.kind) {
1438 case ValueKind.Context:
1439 case ValueKind.Mutable:
1440 case ValueKind.MaybeFrozen: {
1441 const values = this.values(place);
1442 for (const instrValue of values) {
1443 this.freezeValue(instrValue, reason);
1444 }
1445 return true;
1446 }
1447 case ValueKind.Frozen:
1448 case ValueKind.Global:
1449 case ValueKind.Primitive: {
1450 return false;
1451 }
1452 default: {
1453 assertExhaustive(
1454 value.kind,
1455 `Unexpected value kind '${(value as any).kind}'`,
1456 );
1457 }
1458 }
1459 }
1460
1461 freezeValue(value: InstructionValue, reason: ValueReason): void {
1462 this.#values.set(value, {
1463 kind: ValueKind.Frozen,
1464 reason: new Set([reason]),
1465 });
1466 if (
1467 value.kind === 'FunctionExpression' &&
1468 (this.env.config.enablePreserveExistingMemoizationGuarantees ||
1469 this.env.config.enableTransitivelyFreezeFunctionExpressions)
1470 ) {
1471 for (const place of value.loweredFunc.func.context) {
1472 this.freeze(place, reason);
1473 }
1474 }
1475 }
1476
1477 mutate(
1478 variant:
1479 | 'Mutate'
1480 | 'MutateConditionally'
1481 | 'MutateTransitive'
1482 | 'MutateTransitiveConditionally',
1483 place: Place,
1484 ): 'none' | 'mutate' | 'mutate-frozen' | 'mutate-global' | 'mutate-ref' {
1485 if (isRefOrRefValue(place.identifier)) {
1486 return 'mutate-ref';
1487 }
1488 const kind = this.kind(place).kind;
1489 switch (variant) {
1490 case 'MutateConditionally':
1491 case 'MutateTransitiveConditionally': {
1492 switch (kind) {
1493 case ValueKind.Mutable:
1494 case ValueKind.Context: {
1495 return 'mutate';
1496 }
1497 default: {
1498 return 'none';
1499 }
1500 }
1501 }
1502 case 'Mutate':
1503 case 'MutateTransitive': {
1504 switch (kind) {
1505 case ValueKind.Mutable:
1506 case ValueKind.Context: {
1507 return 'mutate';
1508 }
1509 case ValueKind.Primitive: {
1510 // technically an error, but it's not React specific
1511 return 'none';
1512 }
1513 case ValueKind.Frozen: {
1514 return 'mutate-frozen';
1515 }
1516 case ValueKind.Global: {
1517 return 'mutate-global';
1518 }
1519 case ValueKind.MaybeFrozen: {
1520 return 'mutate-frozen';
1521 }
1522 default: {
1523 assertExhaustive(kind, `Unexpected kind ${kind}`);
1524 }
1525 }
1526 }
1527 default: {
1528 assertExhaustive(variant, `Unexpected mutation variant ${variant}`);
1529 }
1530 }
1531 }
1532
1533 /*
1534 * Combine the contents of @param this and @param other, returning a new
1535 * instance with the combined changes _if_ there are any changes, or
1536 * returning null if no changes would occur. Changes include:
1537 * - new entries in @param other that did not exist in @param this
1538 * - entries whose values differ in @param this and @param other,
1539 * and where joining the values produces a different value than
1540 * what was in @param this.
1541 *
1542 * Note that values are joined using a lattice operation to ensure
1543 * termination.
1544 */
1545 merge(other: InferenceState): InferenceState | null {
1546 let nextValues: Map<InstructionValue, AbstractValue> | null = null;
1547 let nextVariables: Map<IdentifierId, Set<InstructionValue>> | null = null;
1548
1549 for (const [id, thisValue] of this.#values) {
1550 const otherValue = other.#values.get(id);
1551 if (otherValue !== undefined) {
1552 const mergedValue = mergeAbstractValues(thisValue, otherValue);
1553 if (mergedValue !== thisValue) {
1554 nextValues = nextValues ?? new Map(this.#values);
1555 nextValues.set(id, mergedValue);
1556 }
1557 }
1558 }
1559 for (const [id, otherValue] of other.#values) {
1560 if (this.#values.has(id)) {
1561 // merged above
1562 continue;
1563 }
1564 nextValues = nextValues ?? new Map(this.#values);
1565 nextValues.set(id, otherValue);
1566 }
1567
1568 for (const [id, thisValues] of this.#variables) {
1569 const otherValues = other.#variables.get(id);
1570 if (otherValues !== undefined) {
1571 let mergedValues: Set<InstructionValue> | null = null;
1572 for (const otherValue of otherValues) {
1573 if (!thisValues.has(otherValue)) {
1574 mergedValues = mergedValues ?? new Set(thisValues);
1575 mergedValues.add(otherValue);
1576 }
1577 }
1578 if (mergedValues !== null) {
1579 nextVariables = nextVariables ?? new Map(this.#variables);
1580 nextVariables.set(id, mergedValues);
1581 }
1582 }
1583 }
1584 for (const [id, otherValues] of other.#variables) {
1585 if (this.#variables.has(id)) {
1586 continue;
1587 }
1588 nextVariables = nextVariables ?? new Map(this.#variables);
1589 nextVariables.set(id, new Set(otherValues));
1590 }
1591
1592 if (nextVariables === null && nextValues === null) {
1593 return null;
1594 } else {
1595 return new InferenceState(
1596 this.env,
1597 this.#isFunctionExpression,
1598 nextValues ?? new Map(this.#values),
1599 nextVariables ?? new Map(this.#variables),
1600 );
1601 }
1602 }
1603
1604 /*
1605 * Returns a copy of this state.
1606 * TODO: consider using persistent data structures to make
1607 * clone cheaper.
1608 */
1609 clone(): InferenceState {
1610 return new InferenceState(
1611 this.env,
1612 this.#isFunctionExpression,
1613 new Map(this.#values),
1614 new Map(this.#variables),
1615 );
1616 }
1617
1618 /*
1619 * For debugging purposes, dumps the state to a plain
1620 * object so that it can printed as JSON.
1621 */
1622 debug(): any {
1623 const result: any = {values: {}, variables: {}};
1624 const objects: Map<InstructionValue, number> = new Map();
1625 function identify(value: InstructionValue): number {
1626 let id = objects.get(value);
1627 if (id == null) {
1628 id = objects.size;
1629 objects.set(value, id);
1630 }
1631 return id;
1632 }
1633 for (const [value, kind] of this.#values) {
1634 const id = identify(value);
1635 result.values[id] = {
1636 abstract: this.debugAbstractValue(kind),
1637 value: printInstructionValue(value),
1638 };
1639 }
1640 for (const [variable, values] of this.#variables) {
1641 result.variables[`$${variable}`] = [...values].map(identify);
1642 }
1643 return result;
1644 }
1645
1646 debugAbstractValue(value: AbstractValue): any {
1647 return {
1648 kind: value.kind,
1649 reason: [...value.reason],
1650 };
1651 }
1652
1653 inferPhi(phi: Phi): void {
1654 const values: Set<InstructionValue> = new Set();
1655 for (const [_, operand] of phi.operands) {
1656 const operandValues = this.#variables.get(operand.identifier.id);
1657 // This is a backedge that will be handled later by State.merge
1658 if (operandValues === undefined) continue;
1659 for (const v of operandValues) {
1660 values.add(v);
1661 }
1662 }
1663
1664 if (values.size > 0) {
1665 this.#variables.set(phi.place.identifier.id, values);
1666 }
1667 }
1668 }
1669
1670 /**
1671 * Returns a value that represents the combined states of the two input values.
1672 * If the two values are semantically equivalent, it returns the first argument.
1673 */
1674 function mergeAbstractValues(
1675 a: AbstractValue,
1676 b: AbstractValue,
1677 ): AbstractValue {
1678 const kind = mergeValueKinds(a.kind, b.kind);
1679 if (
1680 kind === a.kind &&
1681 kind === b.kind &&
1682 Set_isSuperset(a.reason, b.reason)
1683 ) {
1684 return a;
1685 }
1686 const reason = new Set(a.reason);
1687 for (const r of b.reason) {
1688 reason.add(r);
1689 }
1690 return {kind, reason};
1691 }
1692
1693 type InstructionSignature = {
1694 effects: ReadonlyArray<AliasingEffect>;
1695 };
1696
1697 function conditionallyMutateIterator(place: Place): AliasingEffect | null {
1698 if (
1699 !(
1700 isArrayType(place.identifier) ||
1701 isSetType(place.identifier) ||
1702 isMapType(place.identifier)
1703 )
1704 ) {
1705 return {
1706 kind: 'MutateTransitiveConditionally',
1707 value: place,
1708 };
1709 }
1710 return null;
1711 }
1712
1713 /**
1714 * Computes an effect signature for the instruction _without_ looking at the inference state,
1715 * and only using the semantics of the instructions and the inferred types. The idea is to make
1716 * it easy to check that the semantics of each instruction are preserved by describing only the
1717 * effects and not making decisions based on the inference state.
1718 *
1719 * Then in applySignature(), above, we refine this signature based on the inference state.
1720 *
1721 * NOTE: this function is designed to be cached so it's only computed once upon first visiting
1722 * an instruction.
1723 */
1724 function computeSignatureForInstruction(
1725 context: Context,
1726 env: Environment,
1727 instr: Instruction,
1728 ): InstructionSignature {
1729 const {lvalue, value} = instr;
1730 const effects: Array<AliasingEffect> = [];
1731 switch (value.kind) {
1732 case 'ArrayExpression': {
1733 effects.push({
1734 kind: 'Create',
1735 into: lvalue,
1736 value: ValueKind.Mutable,
1737 reason: ValueReason.Other,
1738 });
1739 // All elements are captured into part of the output value
1740 for (const element of value.elements) {
1741 if (element.kind === 'Identifier') {
1742 effects.push({
1743 kind: 'Capture',
1744 from: element,
1745 into: lvalue,
1746 });
1747 } else if (element.kind === 'Spread') {
1748 const mutateIterator = conditionallyMutateIterator(element.place);
1749 if (mutateIterator != null) {
1750 effects.push(mutateIterator);
1751 }
1752 effects.push({
1753 kind: 'Capture',
1754 from: element.place,
1755 into: lvalue,
1756 });
1757 } else {
1758 continue;
1759 }
1760 }
1761 break;
1762 }
1763 case 'ObjectExpression': {
1764 effects.push({
1765 kind: 'Create',
1766 into: lvalue,
1767 value: ValueKind.Mutable,
1768 reason: ValueReason.Other,
1769 });
1770 for (const property of value.properties) {
1771 if (property.kind === 'ObjectProperty') {
1772 effects.push({
1773 kind: 'Capture',
1774 from: property.place,
1775 into: lvalue,
1776 });
1777 } else {
1778 effects.push({
1779 kind: 'Capture',
1780 from: property.place,
1781 into: lvalue,
1782 });
1783 }
1784 }
1785 break;
1786 }
1787 case 'Await': {
1788 effects.push({
1789 kind: 'Create',
1790 into: lvalue,
1791 value: ValueKind.Mutable,
1792 reason: ValueReason.Other,
1793 });
1794 // Potentially mutates the receiver (awaiting it changes its state and can run side effects)
1795 effects.push({kind: 'MutateTransitiveConditionally', value: value.value});
1796 /**
1797 * Data from the promise may be returned into the result, but await does not directly return
1798 * the promise itself
1799 */
1800 effects.push({
1801 kind: 'Capture',
1802 from: value.value,
1803 into: lvalue,
1804 });
1805 break;
1806 }
1807 case 'NewExpression':
1808 case 'CallExpression':
1809 case 'MethodCall': {
1810 let callee;
1811 let receiver;
1812 let mutatesCallee;
1813 if (value.kind === 'NewExpression') {
1814 callee = value.callee;
1815 receiver = value.callee;
1816 mutatesCallee = false;
1817 } else if (value.kind === 'CallExpression') {
1818 callee = value.callee;
1819 receiver = value.callee;
1820 mutatesCallee = true;
1821 } else if (value.kind === 'MethodCall') {
1822 callee = value.property;
1823 receiver = value.receiver;
1824 mutatesCallee = false;
1825 } else {
1826 assertExhaustive(
1827 value,
1828 `Unexpected value kind '${(value as any).kind}'`,
1829 );
1830 }
1831 const signature = getFunctionCallSignature(env, callee.identifier.type);
1832 effects.push({
1833 kind: 'Apply',
1834 receiver,
1835 function: callee,
1836 mutatesFunction: mutatesCallee,
1837 args: value.args,
1838 into: lvalue,
1839 signature,
1840 loc: value.loc,
1841 });
1842 break;
1843 }
1844 case 'PropertyDelete':
1845 case 'ComputedDelete': {
1846 effects.push({
1847 kind: 'Create',
1848 into: lvalue,
1849 value: ValueKind.Primitive,
1850 reason: ValueReason.Other,
1851 });
1852 // Mutates the object by removing the property, no aliasing
1853 effects.push({kind: 'Mutate', value: value.object});
1854 break;
1855 }
1856 case 'PropertyLoad':
1857 case 'ComputedLoad': {
1858 if (isPrimitiveType(lvalue.identifier)) {
1859 effects.push({
1860 kind: 'Create',
1861 into: lvalue,
1862 value: ValueKind.Primitive,
1863 reason: ValueReason.Other,
1864 });
1865 } else {
1866 effects.push({
1867 kind: 'CreateFrom',
1868 from: value.object,
1869 into: lvalue,
1870 });
1871 }
1872 break;
1873 }
1874 case 'PropertyStore':
1875 case 'ComputedStore': {
1876 /**
1877 * Add a hint about naming as "ref"/"-Ref", but only if we weren't able to infer any
1878 * type for the object. In some cases the variable may be named like a ref, but is
1879 * also used as a ref callback such that we infer the type as a function rather than
1880 * a ref.
1881 */
1882 const mutationReason: MutationReason | null =
1883 value.kind === 'PropertyStore' &&
1884 value.property === 'current' &&
1885 value.object.identifier.type.kind === 'Type'
1886 ? {kind: 'AssignCurrentProperty'}
1887 : null;
1888 effects.push({
1889 kind: 'Mutate',
1890 value: value.object,
1891 reason: mutationReason,
1892 });
1893 effects.push({
1894 kind: 'Capture',
1895 from: value.value,
1896 into: value.object,
1897 });
1898 effects.push({
1899 kind: 'Create',
1900 into: lvalue,
1901 value: ValueKind.Primitive,
1902 reason: ValueReason.Other,
1903 });
1904 break;
1905 }
1906 case 'ObjectMethod':
1907 case 'FunctionExpression': {
1908 /**
1909 * We've already analyzed the function expression in AnalyzeFunctions. There, we assign
1910 * a Capture effect to any context variable that appears (locally) to be aliased and/or
1911 * mutated. The precise effects are annotated on the function expression's aliasingEffects
1912 * property, but we don't want to execute those effects yet. We can only use those when
1913 * we know exactly how the function is invoked — via an Apply effect from a custom signature.
1914 *
1915 * But in the general case, functions can be passed around and possibly called in ways where
1916 * we don't know how to interpret their precise effects. For example:
1917 *
1918 * ```
1919 * const a = {};
1920 *
1921 * // We don't want to consider a as mutating here, this just declares the function
1922 * const f = () => { maybeMutate(a) };
1923 *
1924 * // We don't want to consider a as mutating here either, it can't possibly call f yet
1925 * const x = [f];
1926 *
1927 * // Here we have to assume that f can be called (transitively), and have to consider a
1928 * // as mutating
1929 * callAllFunctionInArray(x);
1930 * ```
1931 *
1932 * So for any context variables that were inferred as captured or mutated, we record a
1933 * Capture effect. If the resulting function is transitively mutated, this will mean
1934 * that those operands are also considered mutated. If the function is never called,
1935 * they won't be!
1936 *
1937 * This relies on the rule that:
1938 * Capture a -> b and MutateTransitive(b) => Mutate(a)
1939 *
1940 * Substituting:
1941 * Capture contextvar -> function and MutateTransitive(function) => Mutate(contextvar)
1942 *
1943 * Note that if the type of the context variables are frozen, global, or primitive, the
1944 * Capture will either get pruned or downgraded to an ImmutableCapture.
1945 */
1946 effects.push({
1947 kind: 'CreateFunction',
1948 into: lvalue,
1949 function: value,
1950 captures: value.loweredFunc.func.context.filter(
1951 operand => operand.effect === Effect.Capture,
1952 ),
1953 });
1954 break;
1955 }
1956 case 'GetIterator': {
1957 effects.push({
1958 kind: 'Create',
1959 into: lvalue,
1960 value: ValueKind.Mutable,
1961 reason: ValueReason.Other,
1962 });
1963 if (
1964 isArrayType(value.collection.identifier) ||
1965 isMapType(value.collection.identifier) ||
1966 isSetType(value.collection.identifier)
1967 ) {
1968 /*
1969 * Builtin collections are known to return a fresh iterator on each call,
1970 * so the iterator does not alias the collection
1971 */
1972 effects.push({
1973 kind: 'Capture',
1974 from: value.collection,
1975 into: lvalue,
1976 });
1977 } else {
1978 /*
1979 * Otherwise, the object may return itself as the iterator, so we have to
1980 * assume that the result directly aliases the collection. Further, the
1981 * method to get the iterator could potentially mutate the collection
1982 */
1983 effects.push({kind: 'Alias', from: value.collection, into: lvalue});
1984 effects.push({
1985 kind: 'MutateTransitiveConditionally',
1986 value: value.collection,
1987 });
1988 }
1989 break;
1990 }
1991 case 'IteratorNext': {
1992 /*
1993 * Technically advancing an iterator will always mutate it (for any reasonable implementation)
1994 * But because we create an alias from the collection to the iterator if we don't know the type,
1995 * then it's possible the iterator is aliased to a frozen value and we wouldn't want to error.
1996 * so we mark this as conditional mutation to allow iterating frozen values.
1997 */
1998 effects.push({kind: 'MutateConditionally', value: value.iterator});
1999 // Extracts part of the original collection into the result
2000 effects.push({
2001 kind: 'CreateFrom',
2002 from: value.collection,
2003 into: lvalue,
2004 });
2005 break;
2006 }
2007 case 'NextPropertyOf': {
2008 effects.push({
2009 kind: 'Create',
2010 into: lvalue,
2011 value: ValueKind.Primitive,
2012 reason: ValueReason.Other,
2013 });
2014 break;
2015 }
2016 case 'JsxExpression':
2017 case 'JsxFragment': {
2018 effects.push({
2019 kind: 'Create',
2020 into: lvalue,
2021 value: ValueKind.Frozen,
2022 reason: ValueReason.JsxCaptured,
2023 });
2024 for (const operand of eachInstructionValueOperand(value)) {
2025 effects.push({
2026 kind: 'Freeze',
2027 value: operand,
2028 reason: ValueReason.JsxCaptured,
2029 });
2030 effects.push({
2031 kind: 'Capture',
2032 from: operand,
2033 into: lvalue,
2034 });
2035 }
2036 if (value.kind === 'JsxExpression') {
2037 if (value.tag.kind === 'Identifier') {
2038 // Tags are render function, by definition they're called during render
2039 effects.push({
2040 kind: 'Render',
2041 place: value.tag,
2042 });
2043 }
2044 if (value.children != null) {
2045 // Children are typically called during render, not used as an event/effect callback
2046 for (const child of value.children) {
2047 effects.push({
2048 kind: 'Render',
2049 place: child,
2050 });
2051 }
2052 }
2053 for (const prop of value.props) {
2054 if (
2055 prop.kind === 'JsxAttribute' &&
2056 prop.place.identifier.type.kind === 'Function' &&
2057 (isJsxType(prop.place.identifier.type.return) ||
2058 (prop.place.identifier.type.return.kind === 'Phi' &&
2059 prop.place.identifier.type.return.operands.some(operand =>
2060 isJsxType(operand),
2061 )))
2062 ) {
2063 // Any props which return jsx are assumed to be called during render
2064 effects.push({
2065 kind: 'Render',
2066 place: prop.place,
2067 });
2068 }
2069 }
2070 }
2071 break;
2072 }
2073 case 'DeclareLocal': {
2074 // TODO check this
2075 effects.push({
2076 kind: 'Create',
2077 into: value.lvalue.place,
2078 // TODO: what kind here???
2079 value: ValueKind.Primitive,
2080 reason: ValueReason.Other,
2081 });
2082 effects.push({
2083 kind: 'Create',
2084 into: lvalue,
2085 // TODO: what kind here???
2086 value: ValueKind.Primitive,
2087 reason: ValueReason.Other,
2088 });
2089 break;
2090 }
2091 case 'Destructure': {
2092 for (const patternItem of eachPatternItem(value.lvalue.pattern)) {
2093 const place =
2094 patternItem.kind === 'Identifier' ? patternItem : patternItem.place;
2095 if (isPrimitiveType(place.identifier)) {
2096 effects.push({
2097 kind: 'Create',
2098 into: place,
2099 value: ValueKind.Primitive,
2100 reason: ValueReason.Other,
2101 });
2102 } else if (patternItem.kind === 'Identifier') {
2103 effects.push({
2104 kind: 'CreateFrom',
2105 from: value.value,
2106 into: place,
2107 });
2108 } else {
2109 // Spread creates a new object/array that captures from the RValue
2110 effects.push({
2111 kind: 'Create',
2112 into: place,
2113 reason: ValueReason.Other,
2114 value: context.nonMutatingSpreads.has(place.identifier.id)
2115 ? ValueKind.Frozen
2116 : ValueKind.Mutable,
2117 });
2118 effects.push({
2119 kind: 'Capture',
2120 from: value.value,
2121 into: place,
2122 });
2123 }
2124 }
2125 effects.push({kind: 'Assign', from: value.value, into: lvalue});
2126 break;
2127 }
2128 case 'LoadContext': {
2129 /*
2130 * Context variables are like mutable boxes. Loading from one
2131 * is equivalent to a PropertyLoad from the box, so we model it
2132 * with the same effect we use there (CreateFrom)
2133 */
2134 effects.push({kind: 'CreateFrom', from: value.place, into: lvalue});
2135 break;
2136 }
2137 case 'DeclareContext': {
2138 // Context variables are conceptually like mutable boxes
2139 const kind = value.lvalue.kind;
2140 if (
2141 !context.hoistedContextDeclarations.has(
2142 value.lvalue.place.identifier.declarationId,
2143 ) ||
2144 kind === InstructionKind.HoistedConst ||
2145 kind === InstructionKind.HoistedFunction ||
2146 kind === InstructionKind.HoistedLet
2147 ) {
2148 /**
2149 * If this context variable is not hoisted, or this is the declaration doing the hoisting,
2150 * then we create the box.
2151 */
2152 effects.push({
2153 kind: 'Create',
2154 into: value.lvalue.place,
2155 value: ValueKind.Mutable,
2156 reason: ValueReason.Other,
2157 });
2158 } else {
2159 /**
2160 * Otherwise this may be a "declare", but there was a previous DeclareContext that
2161 * hoisted this variable, and we're mutating it here.
2162 */
2163 effects.push({kind: 'Mutate', value: value.lvalue.place});
2164 }
2165 effects.push({
2166 kind: 'Create',
2167 into: lvalue,
2168 // The result can't be referenced so this value doesn't matter
2169 value: ValueKind.Primitive,
2170 reason: ValueReason.Other,
2171 });
2172 break;
2173 }
2174 case 'StoreContext': {
2175 /*
2176 * Context variables are like mutable boxes, so semantically
2177 * we're either creating (let/const) or mutating (reassign) a box,
2178 * and then capturing the value into it.
2179 */
2180 if (
2181 value.lvalue.kind === InstructionKind.Reassign ||
2182 context.hoistedContextDeclarations.has(
2183 value.lvalue.place.identifier.declarationId,
2184 )
2185 ) {
2186 effects.push({kind: 'Mutate', value: value.lvalue.place});
2187 } else {
2188 effects.push({
2189 kind: 'Create',
2190 into: value.lvalue.place,
2191 value: ValueKind.Mutable,
2192 reason: ValueReason.Other,
2193 });
2194 }
2195 effects.push({
2196 kind: 'Capture',
2197 from: value.value,
2198 into: value.lvalue.place,
2199 });
2200 effects.push({kind: 'Assign', from: value.value, into: lvalue});
2201 break;
2202 }
2203 case 'LoadLocal': {
2204 effects.push({kind: 'Assign', from: value.place, into: lvalue});
2205 break;
2206 }
2207 case 'StoreLocal': {
2208 effects.push({
2209 kind: 'Assign',
2210 from: value.value,
2211 into: value.lvalue.place,
2212 });
2213 effects.push({kind: 'Assign', from: value.value, into: lvalue});
2214 break;
2215 }
2216 case 'PostfixUpdate':
2217 case 'PrefixUpdate': {
2218 effects.push({
2219 kind: 'Create',
2220 into: lvalue,
2221 value: ValueKind.Primitive,
2222 reason: ValueReason.Other,
2223 });
2224 effects.push({
2225 kind: 'Create',
2226 into: value.lvalue,
2227 value: ValueKind.Primitive,
2228 reason: ValueReason.Other,
2229 });
2230 break;
2231 }
2232 case 'StoreGlobal': {
2233 const variable = `\`${value.name}\``;
2234 effects.push({
2235 kind: 'MutateGlobal',
2236 place: value.value,
2237 error: CompilerDiagnostic.create({
2238 category: ErrorCategory.Globals,
2239 reason:
2240 'Cannot reassign variables declared outside of the component/hook',
2241 description: `Variable ${variable} is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)`,
2242 }).withDetails({
2243 kind: 'error',
2244 loc: instr.loc,
2245 message: `${variable} cannot be reassigned`,
2246 }),
2247 });
2248 effects.push({kind: 'Assign', from: value.value, into: lvalue});
2249 break;
2250 }
2251 case 'TypeCastExpression': {
2252 effects.push({kind: 'Assign', from: value.value, into: lvalue});
2253 break;
2254 }
2255 case 'LoadGlobal': {
2256 effects.push({
2257 kind: 'Create',
2258 into: lvalue,
2259 value: ValueKind.Global,
2260 reason: ValueReason.Global,
2261 });
2262 break;
2263 }
2264 case 'StartMemoize':
2265 case 'FinishMemoize': {
2266 if (env.config.enablePreserveExistingMemoizationGuarantees) {
2267 for (const operand of eachInstructionValueOperand(value)) {
2268 effects.push({
2269 kind: 'Freeze',
2270 value: operand,
2271 reason: ValueReason.HookCaptured,
2272 });
2273 }
2274 }
2275 effects.push({
2276 kind: 'Create',
2277 into: lvalue,
2278 value: ValueKind.Primitive,
2279 reason: ValueReason.Other,
2280 });
2281 break;
2282 }
2283 case 'TaggedTemplateExpression':
2284 case 'BinaryExpression':
2285 case 'Debugger':
2286 case 'JSXText':
2287 case 'MetaProperty':
2288 case 'Primitive':
2289 case 'RegExpLiteral':
2290 case 'TemplateLiteral':
2291 case 'UnaryExpression':
2292 case 'UnsupportedNode': {
2293 effects.push({
2294 kind: 'Create',
2295 into: lvalue,
2296 value: ValueKind.Primitive,
2297 reason: ValueReason.Other,
2298 });
2299 break;
2300 }
2301 }
2302 return {
2303 effects,
2304 };
2305 }
2306
2307 /**
2308 * Creates a set of aliasing effects given a legacy FunctionSignature. This makes all of the
2309 * old implicit behaviors from the signatures and InferReferenceEffects explicit, see comments
2310 * in the body for details.
2311 *
2312 * The goal of this method is to make it easier to migrate incrementally to the new system,
2313 * so we don't have to immediately write new signatures for all the methods to get expected
2314 * compilation output.
2315 */
2316 function computeEffectsForLegacySignature(
2317 state: InferenceState,
2318 signature: FunctionSignature,
2319 lvalue: Place,
2320 receiver: Place,
2321 args: Array<Place | SpreadPattern | Hole>,
2322 loc: SourceLocation,
2323 ): Array<AliasingEffect> {
2324 const returnValueReason = signature.returnValueReason ?? ValueReason.Other;
2325 const effects: Array<AliasingEffect> = [];
2326 effects.push({
2327 kind: 'Create',
2328 into: lvalue,
2329 value: signature.returnValueKind,
2330 reason: returnValueReason,
2331 });
2332 if (signature.impure && state.env.config.validateNoImpureFunctionsInRender) {
2333 effects.push({
2334 kind: 'Impure',
2335 place: receiver,
2336 error: CompilerDiagnostic.create({
2337 category: ErrorCategory.Purity,
2338 reason: 'Cannot call impure function during render',
2339 description:
2340 (signature.canonicalName != null
2341 ? `\`${signature.canonicalName}\` is an impure function. `
2342 : '') +
2343 'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
2344 }).withDetails({
2345 kind: 'error',
2346 loc,
2347 message: 'Cannot call impure function',
2348 }),
2349 });
2350 }
2351 if (signature.knownIncompatible != null && state.env.enableValidations) {
2352 const errors = new CompilerError();
2353 errors.pushDiagnostic(
2354 CompilerDiagnostic.create({
2355 category: ErrorCategory.IncompatibleLibrary,
2356 reason: 'Use of incompatible library',
2357 description: [
2358 'This API returns functions which cannot be memoized without leading to stale UI. ' +
2359 'To prevent this, by default React Compiler will skip memoizing this component/hook. ' +
2360 'However, you may see issues if values from this API are passed to other components/hooks that are ' +
2361 'memoized',
2362 ].join(''),
2363 }).withDetails({
2364 kind: 'error',
2365 loc: receiver.loc,
2366 message: signature.knownIncompatible,
2367 }),
2368 );
2369 throw errors;
2370 }
2371 const stores: Array<Place> = [];
2372 const captures: Array<Place> = [];
2373 function visit(place: Place, effect: Effect): void {
2374 switch (effect) {
2375 case Effect.Store: {
2376 effects.push({
2377 kind: 'Mutate',
2378 value: place,
2379 });
2380 stores.push(place);
2381 break;
2382 }
2383 case Effect.Capture: {
2384 captures.push(place);
2385 break;
2386 }
2387 case Effect.ConditionallyMutate: {
2388 effects.push({
2389 kind: 'MutateTransitiveConditionally',
2390 value: place,
2391 });
2392 break;
2393 }
2394 case Effect.ConditionallyMutateIterator: {
2395 const mutateIterator = conditionallyMutateIterator(place);
2396 if (mutateIterator != null) {
2397 effects.push(mutateIterator);
2398 }
2399 effects.push({
2400 kind: 'Capture',
2401 from: place,
2402 into: lvalue,
2403 });
2404 break;
2405 }
2406 case Effect.Freeze: {
2407 effects.push({
2408 kind: 'Freeze',
2409 value: place,
2410 reason: returnValueReason,
2411 });
2412 break;
2413 }
2414 case Effect.Mutate: {
2415 effects.push({kind: 'MutateTransitive', value: place});
2416 break;
2417 }
2418 case Effect.Read: {
2419 effects.push({
2420 kind: 'ImmutableCapture',
2421 from: place,
2422 into: lvalue,
2423 });
2424 break;
2425 }
2426 }
2427 }
2428
2429 if (
2430 signature.mutableOnlyIfOperandsAreMutable &&
2431 areArgumentsImmutableAndNonMutating(state, args)
2432 ) {
2433 effects.push({
2434 kind: 'Alias',
2435 from: receiver,
2436 into: lvalue,
2437 });
2438 for (const arg of args) {
2439 if (arg.kind === 'Hole') {
2440 continue;
2441 }
2442 const place = arg.kind === 'Identifier' ? arg : arg.place;
2443 effects.push({
2444 kind: 'ImmutableCapture',
2445 from: place,
2446 into: lvalue,
2447 });
2448 }
2449 return effects;
2450 }
2451
2452 if (signature.calleeEffect !== Effect.Capture) {
2453 /*
2454 * InferReferenceEffects and FunctionSignature have an implicit assumption that the receiver
2455 * is captured into the return value. Consider for example the signature for Array.proto.pop:
2456 * the calleeEffect is Store, since it's a known mutation but non-transitive. But the return
2457 * of the pop() captures from the receiver! This isn't specified explicitly. So we add this
2458 * here, and rely on applySignature() to downgrade this to ImmutableCapture (or prune) if
2459 * the type doesn't actually need to be captured based on the input and return type.
2460 */
2461 effects.push({
2462 kind: 'Alias',
2463 from: receiver,
2464 into: lvalue,
2465 });
2466 }
2467 visit(receiver, signature.calleeEffect);
2468 for (let i = 0; i < args.length; i++) {
2469 const arg = args[i];
2470 if (arg.kind === 'Hole') {
2471 continue;
2472 }
2473 const place = arg.kind === 'Identifier' ? arg : arg.place;
2474 const signatureEffect =
2475 arg.kind === 'Identifier' && i < signature.positionalParams.length
2476 ? signature.positionalParams[i]!
2477 : (signature.restParam ?? Effect.ConditionallyMutate);
2478 const effect = getArgumentEffect(signatureEffect, arg);
2479
2480 visit(place, effect);
2481 }
2482 if (captures.length !== 0) {
2483 if (stores.length === 0) {
2484 // If no stores, then capture into the return value
2485 for (const capture of captures) {
2486 effects.push({kind: 'Alias', from: capture, into: lvalue});
2487 }
2488 } else {
2489 // Else capture into the stores
2490 for (const capture of captures) {
2491 for (const store of stores) {
2492 effects.push({kind: 'Capture', from: capture, into: store});
2493 }
2494 }
2495 }
2496 }
2497 return effects;
2498 }
2499
2500 /**
2501 * Returns true if all of the arguments are both non-mutable (immutable or frozen)
2502 * _and_ are not functions which might mutate their arguments. Note that function
2503 * expressions count as frozen so long as they do not mutate free variables: this
2504 * function checks that such functions also don't mutate their inputs.
2505 */
2506 function areArgumentsImmutableAndNonMutating(
2507 state: InferenceState,
2508 args: Array<Place | SpreadPattern | Hole>,
2509 ): boolean {
2510 for (const arg of args) {
2511 if (arg.kind === 'Hole') {
2512 continue;
2513 }
2514 if (arg.kind === 'Identifier' && arg.identifier.type.kind === 'Function') {
2515 const fnShape = state.env.getFunctionSignature(arg.identifier.type);
2516 if (fnShape != null) {
2517 return (
2518 !fnShape.positionalParams.some(isKnownMutableEffect) &&
2519 (fnShape.restParam == null ||
2520 !isKnownMutableEffect(fnShape.restParam))
2521 );
2522 }
2523 }
2524 const place = arg.kind === 'Identifier' ? arg : arg.place;
2525
2526 const kind = state.kind(place).kind;
2527 switch (kind) {
2528 case ValueKind.Primitive:
2529 case ValueKind.Frozen: {
2530 /*
2531 * Only immutable values, or frozen lambdas are allowed.
2532 * A lambda may appear frozen even if it may mutate its inputs,
2533 * so we have a second check even for frozen value types
2534 */
2535 break;
2536 }
2537 default: {
2538 /**
2539 * Globals, module locals, and other locally defined functions may
2540 * mutate their arguments.
2541 */
2542 return false;
2543 }
2544 }
2545 const values = state.values(place);
2546 for (const value of values) {
2547 if (
2548 value.kind === 'FunctionExpression' &&
2549 value.loweredFunc.func.params.some(param => {
2550 const place = param.kind === 'Identifier' ? param : param.place;
2551 const range = place.identifier.mutableRange;
2552 return range.end > range.start + 1;
2553 })
2554 ) {
2555 // This is a function which may mutate its inputs
2556 return false;
2557 }
2558 }
2559 }
2560 return true;
2561 }
2562
2563 function computeEffectsForSignature(
2564 env: Environment,
2565 signature: AliasingSignature,
2566 lvalue: Place,
2567 receiver: Place,
2568 args: Array<Place | SpreadPattern | Hole>,
2569 // Used for signatures constructed dynamically which reference context variables
2570 context: Array<Place> = [],
2571 loc: SourceLocation,
2572 ): Array<AliasingEffect> | null {
2573 if (
2574 // Not enough args
2575 signature.params.length > args.length ||
2576 // Too many args and there is no rest param to hold them
2577 (args.length > signature.params.length && signature.rest == null)
2578 ) {
2579 return null;
2580 }
2581 // Build substitutions
2582 const mutableSpreads = new Set<IdentifierId>();
2583 const substitutions: Map<IdentifierId, Array<Place>> = new Map();
2584 substitutions.set(signature.receiver, [receiver]);
2585 substitutions.set(signature.returns, [lvalue]);
2586 const params = signature.params;
2587 for (let i = 0; i < args.length; i++) {
2588 const arg = args[i];
2589 if (arg.kind === 'Hole') {
2590 continue;
2591 } else if (params == null || i >= params.length || arg.kind === 'Spread') {
2592 if (signature.rest == null) {
2593 return null;
2594 }
2595 const place = arg.kind === 'Identifier' ? arg : arg.place;
2596 getOrInsertWith(substitutions, signature.rest, () => []).push(place);
2597
2598 if (arg.kind === 'Spread') {
2599 const mutateIterator = conditionallyMutateIterator(arg.place);
2600 if (mutateIterator != null) {
2601 mutableSpreads.add(arg.place.identifier.id);
2602 }
2603 }
2604 } else {
2605 const param = params[i];
2606 substitutions.set(param, [arg]);
2607 }
2608 }
2609
2610 /*
2611 * Signatures constructed dynamically from function expressions will reference values
2612 * other than their receiver/args/etc. We populate the substitution table with these
2613 * values so that we can still exit for unpopulated substitutions
2614 */
2615 for (const operand of context) {
2616 substitutions.set(operand.identifier.id, [operand]);
2617 }
2618
2619 const effects: Array<AliasingEffect> = [];
2620 for (const signatureTemporary of signature.temporaries) {
2621 const temp = createTemporaryPlace(env, receiver.loc);
2622 substitutions.set(signatureTemporary.identifier.id, [temp]);
2623 }
2624
2625 // Apply substitutions
2626 for (const effect of signature.effects) {
2627 switch (effect.kind) {
2628 case 'MaybeAlias':
2629 case 'Assign':
2630 case 'ImmutableCapture':
2631 case 'Alias':
2632 case 'CreateFrom':
2633 case 'Capture': {
2634 const from = substitutions.get(effect.from.identifier.id) ?? [];
2635 const to = substitutions.get(effect.into.identifier.id) ?? [];
2636 for (const fromId of from) {
2637 for (const toId of to) {
2638 effects.push({
2639 kind: effect.kind,
2640 from: fromId,
2641 into: toId,
2642 });
2643 }
2644 }
2645 break;
2646 }
2647 case 'Impure':
2648 case 'MutateFrozen':
2649 case 'MutateGlobal': {
2650 const values = substitutions.get(effect.place.identifier.id) ?? [];
2651 for (const value of values) {
2652 effects.push({kind: effect.kind, place: value, error: effect.error});
2653 }
2654 break;
2655 }
2656 case 'Render': {
2657 const values = substitutions.get(effect.place.identifier.id) ?? [];
2658 for (const value of values) {
2659 effects.push({kind: effect.kind, place: value});
2660 }
2661 break;
2662 }
2663 case 'Mutate':
2664 case 'MutateTransitive':
2665 case 'MutateTransitiveConditionally':
2666 case 'MutateConditionally': {
2667 const values = substitutions.get(effect.value.identifier.id) ?? [];
2668 for (const id of values) {
2669 effects.push({kind: effect.kind, value: id});
2670 }
2671 break;
2672 }
2673 case 'Freeze': {
2674 const values = substitutions.get(effect.value.identifier.id) ?? [];
2675 for (const value of values) {
2676 if (mutableSpreads.has(value.identifier.id)) {
2677 CompilerError.throwTodo({
2678 reason: 'Support spread syntax for hook arguments',
2679 loc: value.loc,
2680 });
2681 }
2682 effects.push({kind: 'Freeze', value, reason: effect.reason});
2683 }
2684 break;
2685 }
2686 case 'Create': {
2687 const into = substitutions.get(effect.into.identifier.id) ?? [];
2688 for (const value of into) {
2689 effects.push({
2690 kind: 'Create',
2691 into: value,
2692 value: effect.value,
2693 reason: effect.reason,
2694 });
2695 }
2696 break;
2697 }
2698 case 'Apply': {
2699 const applyReceiver = substitutions.get(effect.receiver.identifier.id);
2700 if (applyReceiver == null || applyReceiver.length !== 1) {
2701 return null;
2702 }
2703 const applyFunction = substitutions.get(effect.function.identifier.id);
2704 if (applyFunction == null || applyFunction.length !== 1) {
2705 return null;
2706 }
2707 const applyInto = substitutions.get(effect.into.identifier.id);
2708 if (applyInto == null || applyInto.length !== 1) {
2709 return null;
2710 }
2711 const applyArgs: Array<Place | SpreadPattern | Hole> = [];
2712 for (const arg of effect.args) {
2713 if (arg.kind === 'Hole') {
2714 applyArgs.push(arg);
2715 } else if (arg.kind === 'Identifier') {
2716 const applyArg = substitutions.get(arg.identifier.id);
2717 if (applyArg == null || applyArg.length !== 1) {
2718 return null;
2719 }
2720 applyArgs.push(applyArg[0]);
2721 } else {
2722 const applyArg = substitutions.get(arg.place.identifier.id);
2723 if (applyArg == null || applyArg.length !== 1) {
2724 return null;
2725 }
2726 applyArgs.push({kind: 'Spread', place: applyArg[0]});
2727 }
2728 }
2729 effects.push({
2730 kind: 'Apply',
2731 mutatesFunction: effect.mutatesFunction,
2732 receiver: applyReceiver[0],
2733 args: applyArgs,
2734 function: applyFunction[0],
2735 into: applyInto[0],
2736 signature: effect.signature,
2737 loc,
2738 });
2739 break;
2740 }
2741 case 'CreateFunction': {
2742 CompilerError.throwTodo({
2743 reason: `Support CreateFrom effects in signatures`,
2744 loc: receiver.loc,
2745 });
2746 }
2747 default: {
2748 assertExhaustive(
2749 effect,
2750 `Unexpected effect kind '${(effect as any).kind}'`,
2751 );
2752 }
2753 }
2754 }
2755 return effects;
2756 }
2757
2758 function buildSignatureFromFunctionExpression(
2759 env: Environment,
2760 fn: FunctionExpression,
2761 ): AliasingSignature {
2762 let rest: IdentifierId | null = null;
2763 const params: Array<IdentifierId> = [];
2764 for (const param of fn.loweredFunc.func.params) {
2765 if (param.kind === 'Identifier') {
2766 params.push(param.identifier.id);
2767 } else {
2768 rest = param.place.identifier.id;
2769 }
2770 }
2771 return {
2772 receiver: makeIdentifierId(0),
2773 params,
2774 rest: rest ?? createTemporaryPlace(env, fn.loc).identifier.id,
2775 returns: fn.loweredFunc.func.returns.identifier.id,
2776 effects: fn.loweredFunc.func.aliasingEffects ?? [],
2777 temporaries: [],
2778 };
2779 }
2780
2781 export type AbstractValue = {
2782 kind: ValueKind;
2783 reason: ReadonlySet<ValueReason>;
2784 };
2785
2786 export function getWriteErrorReason(abstractValue: AbstractValue): string {
2787 if (abstractValue.reason.has(ValueReason.Global)) {
2788 return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
2789 } else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
2790 return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
2791 } else if (abstractValue.reason.has(ValueReason.Context)) {
2792 return `Modifying a value returned from 'useContext()' is not allowed.`;
2793 } else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
2794 return 'Modifying a value returned from a function whose return value should not be mutated';
2795 } else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
2796 return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
2797 } else if (abstractValue.reason.has(ValueReason.State)) {
2798 return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead";
2799 } else if (abstractValue.reason.has(ValueReason.ReducerState)) {
2800 return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
2801 } else if (abstractValue.reason.has(ValueReason.Effect)) {
2802 return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
2803 } else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
2804 return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
2805 } else if (abstractValue.reason.has(ValueReason.HookReturn)) {
2806 return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
2807 } else {
2808 return 'This modifies a variable that React considers immutable';
2809 }
2810 }
2811
2812 function getArgumentEffect(
2813 signatureEffect: Effect | null,
2814 arg: Place | SpreadPattern,
2815 ): Effect {
2816 if (signatureEffect != null) {
2817 if (arg.kind === 'Identifier') {
2818 return signatureEffect;
2819 } else if (
2820 signatureEffect === Effect.Mutate ||
2821 signatureEffect === Effect.ConditionallyMutate
2822 ) {
2823 return signatureEffect;
2824 } else {
2825 // see call-spread-argument-mutable-iterator test fixture
2826 if (signatureEffect === Effect.Freeze) {
2827 CompilerError.throwTodo({
2828 reason: 'Support spread syntax for hook arguments',
2829 loc: arg.place.loc,
2830 });
2831 }
2832 // effects[i] is Effect.Capture | Effect.Read | Effect.Store
2833 return Effect.ConditionallyMutateIterator;
2834 }
2835 } else {
2836 return Effect.ConditionallyMutate;
2837 }
2838 }
2839
2840 export function getFunctionCallSignature(
2841 env: Environment,
2842 type: Type,
2843 ): FunctionSignature | null {
2844 if (type.kind !== 'Function') {
2845 return null;
2846 }
2847 return env.getFunctionSignature(type);
2848 }
2849
2850 export function isKnownMutableEffect(effect: Effect): boolean {
2851 switch (effect) {
2852 case Effect.Store:
2853 case Effect.ConditionallyMutate:
2854 case Effect.ConditionallyMutateIterator:
2855 case Effect.Mutate: {
2856 return true;
2857 }
2858
2859 case Effect.Unknown: {
2860 CompilerError.invariant(false, {
2861 reason: 'Unexpected unknown effect',
2862 loc: GeneratedSource,
2863 });
2864 }
2865 case Effect.Read:
2866 case Effect.Capture:
2867 case Effect.Freeze: {
2868 return false;
2869 }
2870 default: {
2871 assertExhaustive(effect, `Unexpected effect \`${effect}\``);
2872 }
2873 }
2874 }
2875
2876 /**
2877 * Joins two values using the following rules:
2878 * == Effect Transitions ==
2879 *
2880 * Freezing an immutable value has not effect:
2881 * ┌───────────────┐
2882 * │ │
2883 * ▼ │ Freeze
2884 * ┌──────────────────────────┐ │
2885 * │ Immutable │──┘
2886 * └──────────────────────────┘
2887 *
2888 * Freezing a mutable or maybe-frozen value makes it frozen. Freezing a frozen
2889 * value has no effect:
2890 * ┌───────────────┐
2891 * ┌─────────────────────────┐ Freeze │ │
2892 * │ MaybeFrozen │────┐ ▼ │ Freeze
2893 * └─────────────────────────┘ │ ┌──────────────────────────┐ │
2894 * ├────▶│ Frozen │──┘
2895 * │ └──────────────────────────┘
2896 * ┌─────────────────────────┐ │
2897 * │ Mutable │────┘
2898 * └─────────────────────────┘
2899 *
2900 * == Join Lattice ==
2901 * - immutable | mutable => mutable
2902 * The justification is that immutable and mutable values are different types,
2903 * and functions can introspect them to tell the difference (if the argument
2904 * is null return early, else if its an object mutate it).
2905 * - frozen | mutable => maybe-frozen
2906 * Frozen values are indistinguishable from mutable values at runtime, so callers
2907 * cannot dynamically avoid mutation of "frozen" values. If a value could be
2908 * frozen we have to distinguish it from a mutable value. But it also isn't known
2909 * frozen yet, so we distinguish as maybe-frozen.
2910 * - immutable | frozen => frozen
2911 * This is subtle and falls out of the above rules. If a value could be any of
2912 * immutable, mutable, or frozen, then at runtime it could either be a primitive
2913 * or a reference type, and callers can't distinguish frozen or not for reference
2914 * types. To ensure that any sequence of joins btw those three states yields the
2915 * correct maybe-frozen, these two have to produce a frozen value.
2916 * - <any> | maybe-frozen => maybe-frozen
2917 * - immutable | context => context
2918 * - mutable | context => context
2919 * - frozen | context => maybe-frozen
2920 *
2921 * ┌──────────────────────────┐
2922 * │ Immutable │───┐
2923 * └──────────────────────────┘ │
2924 * │ ┌─────────────────────────┐
2925 * ├───▶│ Frozen │──┐
2926 * ┌──────────────────────────┐ │ └─────────────────────────┘ │
2927 * │ Frozen │───┤ │ ┌─────────────────────────┐
2928 * └──────────────────────────┘ │ ├─▶│ MaybeFrozen │
2929 * │ ┌─────────────────────────┐ │ └─────────────────────────┘
2930 * ├───▶│ MaybeFrozen │──┘
2931 * ┌──────────────────────────┐ │ └─────────────────────────┘
2932 * │ Mutable │───┘
2933 * └──────────────────────────┘
2934 */
2935 function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
2936 if (a === b) {
2937 return a;
2938 } else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
2939 return ValueKind.MaybeFrozen;
2940 // after this a and b differ and neither are MaybeFrozen
2941 } else if (a === ValueKind.Mutable || b === ValueKind.Mutable) {
2942 if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
2943 // frozen | mutable
2944 return ValueKind.MaybeFrozen;
2945 } else if (a === ValueKind.Context || b === ValueKind.Context) {
2946 // context | mutable
2947 return ValueKind.Context;
2948 } else {
2949 // mutable | immutable
2950 return ValueKind.Mutable;
2951 }
2952 } else if (a === ValueKind.Context || b === ValueKind.Context) {
2953 if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
2954 // frozen | context
2955 return ValueKind.MaybeFrozen;
2956 } else {
2957 // context | immutable
2958 return ValueKind.Context;
2959 }
2960 } else if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
2961 return ValueKind.Frozen;
2962 } else if (a === ValueKind.Global || b === ValueKind.Global) {
2963 return ValueKind.Global;
2964 } else {
2965 CompilerError.invariant(
2966 a === ValueKind.Primitive && b == ValueKind.Primitive,
2967 {
2968 reason: `Unexpected value kind in mergeValues()`,
2969 description: `Found kinds ${a} and ${b}`,
2970 loc: GeneratedSource,
2971 },
2972 );
2973 return ValueKind.Primitive;
2974 }
2975 }