1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 */78import {9 CompilerDiagnostic,10 CompilerError,11 Effect,12 SourceLocation,13 ValueKind,14} from '..';15import {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';42import {43 eachInstructionValueOperand,44 eachPatternItem,45 eachTerminalOperand,46 eachTerminalSuccessor,47} from '../HIR/visitors';4849import {50 assertExhaustive,51 getOrInsertDefault,52 getOrInsertWith,53 Set_isSuperset,54} from '../Utils/utils';55import {56 printAliasingEffect,57 printAliasingSignature,58 printIdentifier,59 printInstruction,60 printInstructionValue,61 printPlace,62} from '../HIR/PrintHIR';63import {FunctionSignature} from '../HIR/ObjectShape';64import prettyFormat from 'pretty-format';65import {createTemporaryPlace} from '../HIR/HIRBuilder';66import {67 AliasingEffect,68 AliasingSignature,69 hashEffect,70 MutationReason,71} from './AliasingEffects';72import {ErrorCategory} from '../CompilerError';7374const DEBUG = false;7576/**77 * Infers the mutation/aliasing effects for instructions and terminals and annotates78 * them on the HIR, making the effects of builtin instructions/functions as well as79 * user-defined functions explicit. These effects then form the basis for subsequent80 * analysis to determine the mutable range of each value in the program — the set of81 * instructions over which the value is created and mutated — as well as validation82 * 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 instruction86 * and the types involved. These candidate effects are cached the first time each87 * instruction is visited. The idea is to reason about the semantics of the instruction88 * or function in isolation, separately from how those effects may interact with later89 * 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 mutable95 * value. A "Mutate <y>" effect may get converted into a "MutateFrozen <error>" effect96 * if y is mutable, etc.97 */98export function inferMutationAliasingEffects(99 fn: HIRFunction,100 {isFunctionExpression}: {isFunctionExpression: boolean} = {101 isFunctionExpression: false,102 },103): void {104 const initialState = InferenceState.empty(fn.env, isFunctionExpression);105106 // Map of blocks to the last (merged) incoming state that was processed107 const statesByBlock: Map<BlockId, InferenceState> = new Map();108109 for (const ref of fn.context) {110 // TODO: using InstructionValue as a bit of a hack, but it's pragmatic111 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 }122123 const paramKind: AbstractValue = isFunctionExpression124 ? {125 kind: ValueKind.Mutable,126 reason: new Set([ValueReason.Other]),127 }128 : {129 kind: ValueKind.Frozen,130 reason: new Set([ValueReason.ReactiveFunctionArgument]),131 };132133 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 }161162 /*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 block172 state = queuedState.merge(state) ?? queuedState;173 queuedStates.set(blockId, state);174 } else {175 /*176 * this is the first queued state for this block, see whether177 * 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);187188 const hoistedContextDeclarations = findHoistedContextDeclarations(fn);189190 const context = new Context(191 isFunctionExpression,192 fn,193 hoistedContextDeclarations,194 findNonMutatedDestructureSpreads(fn),195 );196197 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 }213214 statesByBlock.set(blockId, incomingState);215 const state = incomingState.clone();216 inferBlock(context, state, block);217218 for (const nextBlockId of eachTerminalSuccessor(block.terminal)) {219 queue(nextBlockId, state);220 }221 }222 }223 return;224}225226function 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) == null234 ) {235 // If this is the first load of the value, store the location236 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.HoistedLet247 ) {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}262263class 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>;279280 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 }291292 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 }304305 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}315316/**317 * Finds objects created via ObjectPattern spread destructuring318 * (`const {x, ...spread} = ...`) where a) the rvalue is known frozen and319 * b) the spread value cannot possibly be directly mutated. The idea is that320 * 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 treat333 * it as mutable: `otherProps.foo` only reads a value that must be frozen, so it334 * can be treated as frozen too.335 */336function 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 }350351 // Map of temporaries to identifiers for spread objects352 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.Const373 ) ||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 them414 break;415 }416 case 'PropertyLoad': {417 // Properties must be frozen since the original value was frozen418 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 frozen426 if (!isRefOrRefValue(lvalue.identifier)) {427 knownFrozen.add(lvalue.identifier.id);428 }429 } else {430 // Non-hook calls check their operands, since they are potentially mutable431 if (candidateNonMutatingSpreads.size !== 0) {432 // Otherwise any reference to the spread object itself may mutate433 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 mutate448 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 }461462 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}470471function 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}485486function inferBlock(487 context: Context,488 state: InferenceState,489 block: BasicBlock,490): void {491 for (const phi of block.phis) {492 state.inferPhi(phi);493 }494495 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 error527 * itself. For example, `c = a.b` can throw if `a` is nullish, but the thrown value528 * is an error object synthesized by the JS runtime. Whereas `throwsInput(x)` can529 * throw (effectively) the result of the call.530 *531 * TODO: call applyEffect() instead. This meant that the catch param wasn't inferred532 * as a mutable value, though. See `try-catch-try-value-modified-in-catch-escaping.js`533 * fixture as an example534 */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}562563/**564 * Applies the signature to the given state to determine the precise set of effects565 * that will occur in practice. This takes into account the inferred state of each566 * 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 mutable568 * 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 */572function 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 mutating581 * a known-frozen value.582 *583 * TODO: make sure we're also validating against global mutations somewhere, but584 * 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 }640641 /*642 * Track which values we've already aliased once, so that we can switch to643 * appendAlias() for subsequent aliases into the same value644 */645 const initialized = new Set<IdentifierId>();646647 if (DEBUG) {648 console.log(printInstruction(instruction));649 }650651 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}672673function 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);699700 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 types723 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);738739 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);798799 effects.push(effect);800 /**801 * We consider the function mutable if it has any mutable context variables or802 * 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 compatibility824 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.Global837 ) {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 value878 * within another. If the destination is not mutable, or the source value has879 * copy-on-write semantics, then we can prune the effect880 */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 }915916 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);954955 /*956 * Alias represents potential pointer aliasing. If the type is a global,957 * a primitive (copy-on-write semantics) then we can prune the effect958 */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 != null1022 ) {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 params1026 */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 instruction1119 * 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 flow1149 {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 another1173 */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-op1195 } 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 }12041205 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 });12361237 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.Frozen1282 ? '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}13091310class InferenceState {1311 env: Environment;1312 #isFunctionExpression: boolean;13131314 // The kind of each value, based on its allocation site1315 #values: Map<InstructionValue, AbstractValue>;1316 /*1317 * The set of values pointed to by each identifier. This is a set1318 * to accommodate phi points (where a variable may have different1319 * values from different control flow paths).1320 */1321 #variables: Map<IdentifierId, Set<InstructionValue>>;13221323 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 }13341335 static empty(1336 env: Environment,1337 isFunctionExpression: boolean,1338 ): InferenceState {1339 return new InferenceState(env, isFunctionExpression, new Map(), new Map());1340 }13411342 get isFunctionExpression(): boolean {1343 return this.#isFunctionExpression;1344 }13451346 // (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 }13551356 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 }13661367 // 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 }13891390 // 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 }14011402 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 }14161417 // 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 }14261427 isDefined(place: Place): boolean {1428 return this.#variables.has(place.identifier.id);1429 }14301431 /**1432 * Marks @param place as transitively frozen. Returns true if the value was not1433 * 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 }14601461 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 }14761477 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 specific1511 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 }15321533 /*1534 * Combine the contents of @param this and @param other, returning a new1535 * instance with the combined changes _if_ there are any changes, or1536 * returning null if no changes would occur. Changes include:1537 * - new entries in @param other that did not exist in @param this1538 * - entries whose values differ in @param this and @param other,1539 * and where joining the values produces a different value than1540 * what was in @param this.1541 *1542 * Note that values are joined using a lattice operation to ensure1543 * 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;15481549 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 above1562 continue;1563 }1564 nextValues = nextValues ?? new Map(this.#values);1565 nextValues.set(id, otherValue);1566 }15671568 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 }15911592 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 }16031604 /*1605 * Returns a copy of this state.1606 * TODO: consider using persistent data structures to make1607 * 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 }16171618 /*1619 * For debugging purposes, dumps the state to a plain1620 * 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 }16451646 debugAbstractValue(value: AbstractValue): any {1647 return {1648 kind: value.kind,1649 reason: [...value.reason],1650 };1651 }16521653 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.merge1658 if (operandValues === undefined) continue;1659 for (const v of operandValues) {1660 values.add(v);1661 }1662 }16631664 if (values.size > 0) {1665 this.#variables.set(phi.place.identifier.id, values);1666 }1667 }1668}16691670/**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 */1674function 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}16921693type InstructionSignature = {1694 effects: ReadonlyArray<AliasingEffect>;1695};16961697function 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}17121713/**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 make1716 * it easy to check that the semantics of each instruction are preserved by describing only the1717 * 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 visiting1722 * an instruction.1723 */1724function 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 value1740 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 return1798 * the promise itself1799 */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 aliasing1853 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 any1878 * type for the object. In some cases the variable may be named like a ref, but is1879 * also used as a ref callback such that we infer the type as a function rather than1880 * 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 assign1910 * a Capture effect to any context variable that appears (locally) to be aliased and/or1911 * mutated. The precise effects are annotated on the function expression's aliasingEffects1912 * property, but we don't want to execute those effects yet. We can only use those when1913 * 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 where1916 * 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 function1922 * const f = () => { maybeMutate(a) };1923 *1924 * // We don't want to consider a as mutating here either, it can't possibly call f yet1925 * const x = [f];1926 *1927 * // Here we have to assume that f can be called (transitively), and have to consider a1928 * // as mutating1929 * callAllFunctionInArray(x);1930 * ```1931 *1932 * So for any context variables that were inferred as captured or mutated, we record a1933 * Capture effect. If the resulting function is transitively mutated, this will mean1934 * 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, the1944 * 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 collection1971 */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 to1980 * assume that the result directly aliases the collection. Further, the1981 * method to get the iterator could potentially mutate the collection1982 */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 result2000 effects.push({
Findings
✓ No findings reported for this file.