compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts TYPESCRIPT 2,976 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 2,976.
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.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.