/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError'; import {Environment} from '../HIR'; import { AbstractValue, BasicBlock, BlockId, CallExpression, NewExpression, Effect, FunctionEffect, GeneratedSource, HIRFunction, IdentifierId, InstructionKind, InstructionValue, MethodCall, Phi, Place, SpreadPattern, TInstruction, Type, ValueKind, ValueReason, isArrayType, isMapType, isMutableEffect, isObjectType, isSetType, } from '../HIR/HIR'; import {FunctionSignature} from '../HIR/ObjectShape'; import { printIdentifier, printMixedHIR, printPlace, printSourceLocation, } from '../HIR/PrintHIR'; import { eachInstructionOperand, eachInstructionValueOperand, eachPatternOperand, eachTerminalOperand, eachTerminalSuccessor, } from '../HIR/visitors'; import {assertExhaustive} from '../Utils/utils'; import { inferTerminalFunctionEffects, inferInstructionFunctionEffects, transformFunctionEffectErrors, } from './InferFunctionEffects'; const UndefinedValue: InstructionValue = { kind: 'Primitive', loc: GeneratedSource, value: undefined, }; /* * For every usage of a value in the given function, infers the effect or action * taken at that reference. Each reference is inferred as exactly one of: * - freeze: this usage freezes the value, ie converts it to frozen. This is only inferred * when the value *may* not already be frozen. * - frozen: the value is known to already be "owned" by React and is therefore already * frozen (permanently and transitively immutable). * - immutable: the value is not owned by React, but is known to be an immutable value * that therefore cannot ever change. * - readonly: the value is not frozen or immutable, but this usage of the value does * not modify it. the value may be mutated by a subsequent reference. Examples include * referencing the operands of a binary expression, or referencing the items/properties * of an array or object literal. * - mutable: the value is not frozen or immutable, and this usage *may* modify it. * Examples include passing a value to as a function argument or assigning into an object. * * Note that the inference follows variable assignment, so assigning a frozen value * to a different value will infer usages of the other variable as frozen as well. * * The inference assumes that the code follows the rules of React: * - React function arguments are frozen (component props, hook arguments). * - Hook arguments are frozen at the point the hook is invoked. * - React function return values are frozen at the point of being returned, * thus the return value of a hook call is frozen. * - JSX represents invocation of a React function (the component) and * therefore all values passed to JSX become frozen at the point the JSX * is created. * * Internally, the inference tracks the approximate type of value held by each variable, * and iterates over the control flow graph. The inferred effect of reach reference is * a combination of the operation performed (ie, assignment into an object mutably uses the * object; an if condition reads the condition) and the type of the value. The types of values * are: * - frozen: can be any type so long as the value is known to be owned by React, permanently * and transitively immutable * - maybe-frozen: the value may or may not be frozen, conditionally depending on control flow. * - immutable: a type with value semantics: primitives, records/tuples when standardized. * - mutable: a type with reference semantics eg array, object, class instance, etc. * * When control flow paths converge the types of values are merged together, with the value * types forming a lattice to ensure convergence. */ export default function inferReferenceEffects( fn: HIRFunction, options: {isFunctionExpression: boolean} = {isFunctionExpression: false}, ): Array { /* * Initial state contains function params * TODO: include module declarations here as well */ const initialState = InferenceState.empty( fn.env, options.isFunctionExpression, ); const value: InstructionValue = { kind: 'Primitive', loc: fn.loc, value: undefined, }; initialState.initialize(value, { kind: ValueKind.Frozen, reason: new Set([ValueReason.Other]), context: new Set(), }); for (const ref of fn.context) { // TODO(gsn): This is a hack. const value: InstructionValue = { kind: 'ObjectExpression', properties: [], loc: ref.loc, }; initialState.initialize(value, { kind: ValueKind.Context, reason: new Set([ValueReason.Other]), context: new Set([ref]), }); initialState.define(ref, value); } const paramKind: AbstractValue = options.isFunctionExpression ? { kind: ValueKind.Mutable, reason: new Set([ValueReason.Other]), context: new Set(), } : { kind: ValueKind.Frozen, reason: new Set([ValueReason.ReactiveFunctionArgument]), context: new Set(), }; if (fn.fnType === 'Component') { CompilerError.invariant(fn.params.length <= 2, { reason: 'Expected React component to have not more than two parameters: one for props and for ref', description: null, loc: fn.loc, suggestions: null, }); const [props, ref] = fn.params; let value: InstructionValue; let place: Place; if (props) { inferParam(props, initialState, paramKind); } if (ref) { if (ref.kind === 'Identifier') { place = ref; value = { kind: 'ObjectExpression', properties: [], loc: ref.loc, }; } else { place = ref.place; value = { kind: 'ObjectExpression', properties: [], loc: ref.place.loc, }; } initialState.initialize(value, { kind: ValueKind.Mutable, reason: new Set([ValueReason.Other]), context: new Set(), }); initialState.define(place, value); } } else { for (const param of fn.params) { inferParam(param, initialState, paramKind); } } // Map of blocks to the last (merged) incoming state that was processed const statesByBlock: Map = new Map(); /* * Multiple predecessors may be visited prior to reaching a given successor, * so track the list of incoming state for each successor block. * These are merged when reaching that block again. */ const queuedStates: Map = new Map(); function queue(blockId: BlockId, state: InferenceState): void { let queuedState = queuedStates.get(blockId); if (queuedState != null) { // merge the queued states for this block state = queuedState.merge(state) ?? queuedState; queuedStates.set(blockId, state); } else { /* * this is the first queued state for this block, see whether * there are changed relative to the last time it was processed. */ const prevState = statesByBlock.get(blockId); const nextState = prevState != null ? prevState.merge(state) : state; if (nextState != null) { queuedStates.set(blockId, nextState); } } } queue(fn.body.entry, initialState); const functionEffects: Array = fn.effects ?? []; while (queuedStates.size !== 0) { for (const [blockId, block] of fn.body.blocks) { const incomingState = queuedStates.get(blockId); queuedStates.delete(blockId); if (incomingState == null) { continue; } statesByBlock.set(blockId, incomingState); const state = incomingState.clone(); inferBlock(fn.env, state, block, functionEffects); for (const nextBlockId of eachTerminalSuccessor(block.terminal)) { queue(nextBlockId, state); } } } if (options.isFunctionExpression) { fn.effects = functionEffects; return []; } else { return transformFunctionEffectErrors(functionEffects); } } type FreezeAction = {values: Set; reason: Set}; // Maintains a mapping of top-level variables to the kind of value they hold class InferenceState { env: Environment; #isFunctionExpression: boolean; // The kind of each value, based on its allocation site #values: Map; /* * The set of values pointed to by each identifier. This is a set * to accomodate phi points (where a variable may have different * values from different control flow paths). */ #variables: Map>; constructor( env: Environment, isFunctionExpression: boolean, values: Map, variables: Map>, ) { this.env = env; this.#isFunctionExpression = isFunctionExpression; this.#values = values; this.#variables = variables; } static empty( env: Environment, isFunctionExpression: boolean, ): InferenceState { return new InferenceState(env, isFunctionExpression, new Map(), new Map()); } get isFunctionExpression(): boolean { return this.#isFunctionExpression; } // (Re)initializes a @param value with its default @param kind. initialize(value: InstructionValue, kind: AbstractValue): void { CompilerError.invariant(value.kind !== 'LoadLocal', { reason: 'Expected all top-level identifiers to be defined as variables, not values', description: null, loc: value.loc, suggestions: null, }); this.#values.set(value, kind); } values(place: Place): Array { const values = this.#variables.get(place.identifier.id); CompilerError.invariant(values != null, { reason: `[hoisting] Expected value kind to be initialized`, description: `${printPlace(place)}`, loc: place.loc, suggestions: null, }); return Array.from(values); } // Lookup the kind of the given @param value. kind(place: Place): AbstractValue { const values = this.#variables.get(place.identifier.id); CompilerError.invariant(values != null, { reason: `[hoisting] Expected value kind to be initialized`, description: `${printPlace(place)}`, loc: place.loc, suggestions: null, }); let mergedKind: AbstractValue | null = null; for (const value of values) { const kind = this.#values.get(value)!; mergedKind = mergedKind !== null ? mergeAbstractValues(mergedKind, kind) : kind; } CompilerError.invariant(mergedKind !== null, { reason: `InferReferenceEffects::kind: Expected at least one value`, description: `No value found at \`${printPlace(place)}\``, loc: place.loc, suggestions: null, }); return mergedKind; } // Updates the value at @param place to point to the same value as @param value. alias(place: Place, value: Place): void { const values = this.#variables.get(value.identifier.id); CompilerError.invariant(values != null, { reason: `[hoisting] Expected value for identifier to be initialized`, description: `${printIdentifier(value.identifier)}`, loc: value.loc, suggestions: null, }); this.#variables.set(place.identifier.id, new Set(values)); } // Defines (initializing or updating) a variable with a specific kind of value. define(place: Place, value: InstructionValue): void { CompilerError.invariant(this.#values.has(value), { reason: `Expected value to be initialized at '${printSourceLocation( value.loc, )}'`, description: null, loc: value.loc, suggestions: null, }); this.#variables.set(place.identifier.id, new Set([value])); } isDefined(place: Place): boolean { return this.#variables.has(place.identifier.id); } /* * Records that a given Place was accessed with the given kind and: * - Updates the effect of @param place based on the kind of value * and the kind of reference (@param effectKind). * - Updates the value kind to reflect the effect of the reference. * * Notably, a mutable reference is downgraded to readonly if the * value unless the value is known to be mutable. * * Similarly, a freeze reference is converted to readonly if the * value is already frozen or is immutable. */ referenceAndRecordEffects( freezeActions: Array, place: Place, effectKind: Effect, reason: ValueReason, ): void { const values = this.#variables.get(place.identifier.id); if (values === undefined) { CompilerError.invariant(effectKind !== Effect.Store, { reason: '[InferReferenceEffects] Unhandled store reference effect', description: null, loc: place.loc, suggestions: null, }); place.effect = effectKind === Effect.ConditionallyMutate ? Effect.ConditionallyMutate : Effect.Read; return; } const action = this.reference(place, effectKind, reason); action && freezeActions.push(action); } freezeValues(values: Set, reason: Set): void { for (const value of values) { if ( value.kind === 'DeclareContext' || (value.kind === 'StoreContext' && (value.lvalue.kind === InstructionKind.Let || value.lvalue.kind === InstructionKind.Const)) ) { /** * Avoid freezing context variable declarations, hoisted or otherwise * function Component() { * const cb = useBar(() => foo(2)); // produces a hoisted context declaration * const foo = useFoo(); // reassigns to the context variable * return ; * } */ continue; } this.#values.set(value, { kind: ValueKind.Frozen, reason, context: new Set(), }); if ( value.kind === 'FunctionExpression' && (this.env.config.enablePreserveExistingMemoizationGuarantees || this.env.config.enableTransitivelyFreezeFunctionExpressions) ) { for (const operand of value.loweredFunc.func.context) { const operandValues = this.#variables.get(operand.identifier.id); if (operandValues !== undefined) { this.freezeValues(operandValues, reason); } } } } } reference( place: Place, effectKind: Effect, reason: ValueReason, ): null | FreezeAction { const values = this.#variables.get(place.identifier.id); CompilerError.invariant(values !== undefined, { reason: '[InferReferenceEffects] Expected value to be initialized', description: null, loc: place.loc, suggestions: null, }); let valueKind: AbstractValue | null = this.kind(place); let effect: Effect | null = null; let freeze: null | FreezeAction = null; switch (effectKind) { case Effect.Freeze: { if ( valueKind.kind === ValueKind.Mutable || valueKind.kind === ValueKind.Context || valueKind.kind === ValueKind.MaybeFrozen ) { const reasonSet = new Set([reason]); effect = Effect.Freeze; valueKind = { kind: ValueKind.Frozen, reason: reasonSet, context: new Set(), }; freeze = {values, reason: reasonSet}; } else { effect = Effect.Read; } break; } case Effect.ConditionallyMutate: { if ( valueKind.kind === ValueKind.Mutable || valueKind.kind === ValueKind.Context ) { effect = Effect.ConditionallyMutate; } else { effect = Effect.Read; } break; } case Effect.ConditionallyMutateIterator: { if ( valueKind.kind === ValueKind.Mutable || valueKind.kind === ValueKind.Context ) { if ( isArrayType(place.identifier) || isSetType(place.identifier) || isMapType(place.identifier) ) { effect = Effect.Capture; } else { effect = Effect.ConditionallyMutate; } } else { effect = Effect.Read; } break; } case Effect.Mutate: { effect = Effect.Mutate; break; } case Effect.Store: { /* * TODO(gsn): This should be bailout once we add bailout infra. * * invariant( * valueKind.kind === ValueKindKind.Mutable, * `expected valueKind to be 'Mutable' but found to be \`${valueKind}\`` * ); */ effect = isObjectType(place.identifier) ? Effect.Store : Effect.Mutate; break; } case Effect.Capture: { if ( valueKind.kind === ValueKind.Primitive || valueKind.kind === ValueKind.Global || valueKind.kind === ValueKind.Frozen || valueKind.kind === ValueKind.MaybeFrozen ) { effect = Effect.Read; } else { effect = Effect.Capture; } break; } case Effect.Read: { effect = Effect.Read; break; } case Effect.Unknown: { CompilerError.invariant(false, { reason: 'Unexpected unknown effect, expected to infer a precise effect kind', description: null, loc: place.loc, suggestions: null, }); } default: { assertExhaustive( effectKind, `Unexpected reference kind \`${effectKind as any as string}\``, ); } } CompilerError.invariant(effect !== null, { reason: 'Expected effect to be set', description: null, loc: place.loc, suggestions: null, }); place.effect = effect; return freeze; } /* * Combine the contents of @param this and @param other, returning a new * instance with the combined changes _if_ there are any changes, or * returning null if no changes would occur. Changes include: * - new entries in @param other that did not exist in @param this * - entries whose values differ in @param this and @param other, * and where joining the values produces a different value than * what was in @param this. * * Note that values are joined using a lattice operation to ensure * termination. */ merge(other: InferenceState): InferenceState | null { let nextValues: Map | null = null; let nextVariables: Map> | null = null; for (const [id, thisValue] of this.#values) { const otherValue = other.#values.get(id); if (otherValue !== undefined) { const mergedValue = mergeAbstractValues(thisValue, otherValue); if (mergedValue !== thisValue) { nextValues = nextValues ?? new Map(this.#values); nextValues.set(id, mergedValue); } } } for (const [id, otherValue] of other.#values) { if (this.#values.has(id)) { // merged above continue; } nextValues = nextValues ?? new Map(this.#values); nextValues.set(id, otherValue); } for (const [id, thisValues] of this.#variables) { const otherValues = other.#variables.get(id); if (otherValues !== undefined) { let mergedValues: Set | null = null; for (const otherValue of otherValues) { if (!thisValues.has(otherValue)) { mergedValues = mergedValues ?? new Set(thisValues); mergedValues.add(otherValue); } } if (mergedValues !== null) { nextVariables = nextVariables ?? new Map(this.#variables); nextVariables.set(id, mergedValues); } } } for (const [id, otherValues] of other.#variables) { if (this.#variables.has(id)) { continue; } nextVariables = nextVariables ?? new Map(this.#variables); nextVariables.set(id, new Set(otherValues)); } if (nextVariables === null && nextValues === null) { return null; } else { return new InferenceState( this.env, this.#isFunctionExpression, nextValues ?? new Map(this.#values), nextVariables ?? new Map(this.#variables), ); } } /* * Returns a copy of this state. * TODO: consider using persistent data structures to make * clone cheaper. */ clone(): InferenceState { return new InferenceState( this.env, this.#isFunctionExpression, new Map(this.#values), new Map(this.#variables), ); } /* * For debugging purposes, dumps the state to a plain * object so that it can printed as JSON. */ debug(): any { const result: any = {values: {}, variables: {}}; const objects: Map = new Map(); function identify(value: InstructionValue): number { let id = objects.get(value); if (id == null) { id = objects.size; objects.set(value, id); } return id; } for (const [value, kind] of this.#values) { const id = identify(value); result.values[id] = {kind, value: printMixedHIR(value)}; } for (const [variable, values] of this.#variables) { result.variables[`$${variable}`] = [...values].map(identify); } return result; } inferPhi(phi: Phi): void { const values: Set = new Set(); for (const [_, operand] of phi.operands) { const operandValues = this.#variables.get(operand.identifier.id); // This is a backedge that will be handled later by State.merge if (operandValues === undefined) continue; for (const v of operandValues) { values.add(v); } } if (values.size > 0) { this.#variables.set(phi.place.identifier.id, values); } } } function inferParam( param: Place | SpreadPattern, initialState: InferenceState, paramKind: AbstractValue, ): void { let value: InstructionValue; let place: Place; if (param.kind === 'Identifier') { place = param; value = { kind: 'Primitive', loc: param.loc, value: undefined, }; } else { place = param.place; value = { kind: 'Primitive', loc: param.place.loc, value: undefined, }; } initialState.initialize(value, paramKind); initialState.define(place, value); } /* * Joins two values using the following rules: * == Effect Transitions == * * Freezing an immutable value has not effect: * ┌───────────────┐ * │ │ * ▼ │ Freeze * ┌──────────────────────────┐ │ * │ Immutable │──┘ * └──────────────────────────┘ * * Freezing a mutable or maybe-frozen value makes it frozen. Freezing a frozen * value has no effect: * ┌───────────────┐ * ┌─────────────────────────┐ Freeze │ │ * │ MaybeFrozen │────┐ ▼ │ Freeze * └─────────────────────────┘ │ ┌──────────────────────────┐ │ * ├────▶│ Frozen │──┘ * │ └──────────────────────────┘ * ┌─────────────────────────┐ │ * │ Mutable │────┘ * └─────────────────────────┘ * * == Join Lattice == * - immutable | mutable => mutable * The justification is that immutable and mutable values are different types, * and functions can introspect them to tell the difference (if the argument * is null return early, else if its an object mutate it). * - frozen | mutable => maybe-frozen * Frozen values are indistinguishable from mutable values at runtime, so callers * cannot dynamically avoid mutation of "frozen" values. If a value could be * frozen we have to distinguish it from a mutable value. But it also isn't known * frozen yet, so we distinguish as maybe-frozen. * - immutable | frozen => frozen * This is subtle and falls out of the above rules. If a value could be any of * immutable, mutable, or frozen, then at runtime it could either be a primitive * or a reference type, and callers can't distinguish frozen or not for reference * types. To ensure that any sequence of joins btw those three states yields the * correct maybe-frozen, these two have to produce a frozen value. * - | maybe-frozen => maybe-frozen * - immutable | context => context * - mutable | context => context * - frozen | context => maybe-frozen * * ┌──────────────────────────┐ * │ Immutable │───┐ * └──────────────────────────┘ │ * │ ┌─────────────────────────┐ * ├───▶│ Frozen │──┐ * ┌──────────────────────────┐ │ └─────────────────────────┘ │ * │ Frozen │───┤ │ ┌─────────────────────────┐ * └──────────────────────────┘ │ ├─▶│ MaybeFrozen │ * │ ┌─────────────────────────┐ │ └─────────────────────────┘ * ├───▶│ MaybeFrozen │──┘ * ┌──────────────────────────┐ │ └─────────────────────────┘ * │ Mutable │───┘ * └──────────────────────────┘ */ function mergeValues(a: ValueKind, b: ValueKind): ValueKind { if (a === b) { return a; } else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) { return ValueKind.MaybeFrozen; // after this a and b differ and neither are MaybeFrozen } else if (a === ValueKind.Mutable || b === ValueKind.Mutable) { if (a === ValueKind.Frozen || b === ValueKind.Frozen) { // frozen | mutable return ValueKind.MaybeFrozen; } else if (a === ValueKind.Context || b === ValueKind.Context) { // context | mutable return ValueKind.Context; } else { // mutable | immutable return ValueKind.Mutable; } } else if (a === ValueKind.Context || b === ValueKind.Context) { if (a === ValueKind.Frozen || b === ValueKind.Frozen) { // frozen | context return ValueKind.MaybeFrozen; } else { // context | immutable return ValueKind.Context; } } else if (a === ValueKind.Frozen || b === ValueKind.Frozen) { return ValueKind.Frozen; } else if (a === ValueKind.Global || b === ValueKind.Global) { return ValueKind.Global; } else { CompilerError.invariant( a === ValueKind.Primitive && b == ValueKind.Primitive, { reason: `Unexpected value kind in mergeValues()`, description: `Found kinds ${a} and ${b}`, loc: GeneratedSource, }, ); return ValueKind.Primitive; } } /** * @returns `true` if `a` is a superset of `b`. */ function isSuperset(a: ReadonlySet, b: ReadonlySet): boolean { for (const v of b) { if (!a.has(v)) { return false; } } return true; } function mergeAbstractValues( a: AbstractValue, b: AbstractValue, ): AbstractValue { const kind = mergeValues(a.kind, b.kind); if ( kind === a.kind && kind === b.kind && isSuperset(a.reason, b.reason) && isSuperset(a.context, b.context) ) { return a; } const reason = new Set(a.reason); for (const r of b.reason) { reason.add(r); } const context = new Set(a.context); for (const c of b.context) { context.add(c); } return {kind, reason, context}; } type Continuation = | { kind: 'initialize'; valueKind: AbstractValue; effect: {kind: Effect; reason: ValueReason} | null; lvalueEffect?: Effect; } | {kind: 'funeffects'}; /* * Iterates over the given @param block, defining variables and * recording references on the @param state according to JS semantics. */ function inferBlock( env: Environment, state: InferenceState, block: BasicBlock, functionEffects: Array, ): void { for (const phi of block.phis) { state.inferPhi(phi); } for (const instr of block.instructions) { const instrValue = instr.value; const defaultLvalueEffect = Effect.ConditionallyMutate; let continuation: Continuation; const freezeActions: Array = []; switch (instrValue.kind) { case 'BinaryExpression': { continuation = { kind: 'initialize', valueKind: { kind: ValueKind.Primitive, reason: new Set([ValueReason.Other]), context: new Set(), }, effect: { kind: Effect.Read, reason: ValueReason.Other, }, }; break; } case 'ArrayExpression': { const contextRefOperands = getContextRefOperand(state, instrValue); const valueKind: AbstractValue = contextRefOperands.length > 0 ? { kind: ValueKind.Context, reason: new Set([ValueReason.Other]), context: new Set(contextRefOperands), } : { kind: ValueKind.Mutable, reason: new Set([ValueReason.Other]), context: new Set(), }; for (const element of instrValue.elements) { if (element.kind === 'Spread') { state.referenceAndRecordEffects( freezeActions, element.place, Effect.ConditionallyMutateIterator, ValueReason.Other, ); } else if (element.kind === 'Identifier') { state.referenceAndRecordEffects( freezeActions, element, Effect.Capture, ValueReason.Other, ); } else { let _: 'Hole' = element.kind; } } state.initialize(instrValue, valueKind); state.define(instr.lvalue, instrValue); instr.lvalue.effect = Effect.Store; continuation = { kind: 'funeffects', }; break; } case 'NewExpression': { inferCallEffects( state, instr as TInstruction, freezeActions, getFunctionCallSignature(env, instrValue.callee.identifier.type), ); continuation = {kind: 'funeffects'}; break; } case 'ObjectExpression': { const contextRefOperands = getContextRefOperand(state, instrValue); const valueKind: AbstractValue = contextRefOperands.length > 0 ? { kind: ValueKind.Context, reason: new Set([ValueReason.Other]), context: new Set(contextRefOperands), } : { kind: ValueKind.Mutable, reason: new Set([ValueReason.Other]), context: new Set(), }; for (const property of instrValue.properties) { switch (property.kind) { case 'ObjectProperty': { if (property.key.kind === 'computed') { // Object keys must be primitives, so we know they're frozen at this point state.referenceAndRecordEffects( freezeActions, property.key.name, Effect.Freeze, ValueReason.Other, ); } // Object construction captures but does not modify the key/property values state.referenceAndRecordEffects( freezeActions, property.place, Effect.Capture, ValueReason.Other, ); break; } case 'Spread': { // Object construction captures but does not modify the key/property values state.referenceAndRecordEffects( freezeActions, property.place, Effect.Capture, ValueReason.Other, ); break; } default: { assertExhaustive( property, `Unexpected property kind \`${(property as any).kind}\``, ); } } } state.initialize(instrValue, valueKind); state.define(instr.lvalue, instrValue); instr.lvalue.effect = Effect.Store; continuation = {kind: 'funeffects'}; break; } case 'UnaryExpression': { continuation = { kind: 'initialize', valueKind: { kind: ValueKind.Primitive, reason: new Set([ValueReason.Other]), context: new Set(), }, effect: {kind: Effect.Read, reason: ValueReason.Other}, }; break; } case 'UnsupportedNode': { // TODO: handle other statement kinds continuation = { kind: 'initialize', valueKind: { kind: ValueKind.Mutable, reason: new Set([ValueReason.Other]), context: new Set(), }, effect: null, }; break; } case 'JsxExpression': { if (instrValue.tag.kind === 'Identifier') { state.referenceAndRecordEffects( freezeActions, instrValue.tag, Effect.Freeze, ValueReason.JsxCaptured, ); } if (instrValue.children !== null) { for (const child of instrValue.children) { state.referenceAndRecordEffects( freezeActions, child, Effect.Freeze, ValueReason.JsxCaptured, ); } } for (const attr of instrValue.props) { if (attr.kind === 'JsxSpreadAttribute') { state.referenceAndRecordEffects( freezeActions, attr.argument, Effect.Freeze, ValueReason.JsxCaptured, ); } else { state.referenceAndRecordEffects( freezeActions, attr.place, Effect.Freeze, ValueReason.JsxCaptured, ); } } state.initialize(instrValue, { kind: ValueKind.Frozen, reason: new Set([ValueReason.Other]), context: new Set(), }); state.define(instr.lvalue, instrValue); instr.lvalue.effect = Effect.ConditionallyMutate; continuation = {kind: 'funeffects'}; break; } case 'JsxFragment': { continuation = { kind: 'initialize', valueKind: { kind: ValueKind.Frozen, reason: new Set([ValueReason.Other]), context: new Set(), }, effect: { kind: Effect.Freeze, reason: ValueReason.Other, }, }; break; } case 'TemplateLiteral': { /* * template literal (with no tag function) always produces * an immutable string */ continuation = { kind: 'initialize', valueKind: { kind: ValueKind.Primitive, reason: new Set([ValueReason.Other]), context: new Set(), }, effect: {kind: Effect.Read, reason: ValueReason.Other}, }; break; } case 'RegExpLiteral': { // RegExp instances are mutable objects continuation = { kind: 'initialize', valueKind: { kind: ValueKind.Mutable, reason: new Set([ValueReason.Other]), context: new Set(), }, effect: { kind: Effect.ConditionallyMutate, reason: ValueReason.Other, }, }; break; } case 'MetaProperty': { if (instrValue.meta !== 'import' || instrValue.property !== 'meta') { continuation = {kind: 'funeffects'}; break; } continuation = { kind: 'initialize', valueKind: { kind: ValueKind.Global, reason: new Set([ValueReason.Global]), context: new Set(), }, effect: null, }; break; } case 'LoadGlobal': continuation = { kind: 'initialize', valueKind: { kind: ValueKind.Global, reason: new Set([ValueReason.Global]), context: new Set(), }, effect: null, }; break; case 'Debugger': case 'JSXText': case 'Primitive': { continuation = { kind: 'initialize', valueKind: { kind: ValueKind.Primitive, reason: new Set([ValueReason.Other]), context: new Set(), }, effect: null, }; break; } case 'ObjectMethod': case 'FunctionExpression': { let hasMutableOperand = false; for (const operand of eachInstructionOperand(instr)) { CompilerError.invariant(operand.effect !== Effect.Unknown, { reason: 'Expected fn effects to be populated', loc: operand.loc, }); state.referenceAndRecordEffects( freezeActions, operand, operand.effect, ValueReason.Other, ); hasMutableOperand ||= isMutableEffect(operand.effect, operand.loc); } /* * If a closure did not capture any mutable values, then we can consider it to be * frozen, which allows it to be independently memoized. */ state.initialize(instrValue, { kind: hasMutableOperand ? ValueKind.Mutable : ValueKind.Frozen, reason: new Set([ValueReason.Other]), context: new Set(), }); state.define(instr.lvalue, instrValue); instr.lvalue.effect = Effect.Store; continuation = {kind: 'funeffects'}; break; } case 'TaggedTemplateExpression': { const operands = [...eachInstructionValueOperand(instrValue)]; if (operands.length !== 1) { // future-proofing to make sure we update this case when we support interpolation CompilerError.throwTodo({ reason: 'Support tagged template expressions with interpolations', loc: instrValue.loc, }); } const signature = getFunctionCallSignature( env, instrValue.tag.identifier.type, ); let calleeEffect = signature?.calleeEffect ?? Effect.ConditionallyMutate; const returnValueKind: AbstractValue = signature !== null ? { kind: signature.returnValueKind, reason: new Set([ signature.returnValueReason ?? ValueReason.KnownReturnSignature, ]), context: new Set(), } : { kind: ValueKind.Mutable, reason: new Set([ValueReason.Other]), context: new Set(), }; state.referenceAndRecordEffects( freezeActions, instrValue.tag, calleeEffect, ValueReason.Other, ); state.initialize(instrValue, returnValueKind); state.define(instr.lvalue, instrValue); instr.lvalue.effect = Effect.ConditionallyMutate; continuation = {kind: 'funeffects'}; break; } case 'CallExpression': { inferCallEffects( state, instr as TInstruction, freezeActions, getFunctionCallSignature(env, instrValue.callee.identifier.type), ); continuation = {kind: 'funeffects'}; break; } case 'MethodCall': { CompilerError.invariant(state.isDefined(instrValue.receiver), { reason: '[InferReferenceEffects] Internal error: receiver of PropertyCall should have been defined by corresponding PropertyLoad', description: null, loc: instrValue.loc, suggestions: null, }); state.referenceAndRecordEffects( freezeActions, instrValue.property, Effect.Read, ValueReason.Other, ); inferCallEffects( state, instr as TInstruction, freezeActions, getFunctionCallSignature(env, instrValue.property.identifier.type), ); continuation = {kind: 'funeffects'}; break; } case 'PropertyStore': { const effect = state.kind(instrValue.object).kind === ValueKind.Context ? Effect.ConditionallyMutate : Effect.Capture; state.referenceAndRecordEffects( freezeActions, instrValue.value, effect, ValueReason.Other, ); state.referenceAndRecordEffects( freezeActions, instrValue.object, Effect.Store, ValueReason.Other, ); const lvalue = instr.lvalue; state.alias(lvalue, instrValue.value); lvalue.effect = Effect.Store; continuation = {kind: 'funeffects'}; break; } case 'PropertyDelete': { // `delete` returns a boolean (immutable) and modifies the object continuation = { kind: 'initialize', valueKind: { kind: ValueKind.Primitive, reason: new Set([ValueReason.Other]), context: new Set(), }, effect: {kind: Effect.Mutate, reason: ValueReason.Other}, }; break; } case 'PropertyLoad': { state.referenceAndRecordEffects( freezeActions, instrValue.object, Effect.Read, ValueReason.Other, ); const lvalue = instr.lvalue; lvalue.effect = Effect.ConditionallyMutate; state.initialize(instrValue, state.kind(instrValue.object)); state.define(lvalue, instrValue); continuation = {kind: 'funeffects'}; break; } case 'ComputedStore': { const effect = state.kind(instrValue.object).kind === ValueKind.Context ? Effect.ConditionallyMutate : Effect.Capture; state.referenceAndRecordEffects( freezeActions, instrValue.value, effect, ValueReason.Other, ); state.referenceAndRecordEffects( freezeActions, instrValue.property, Effect.Capture, ValueReason.Other, ); state.referenceAndRecordEffects( freezeActions, instrValue.object, Effect.Store, ValueReason.Other, ); const lvalue = instr.lvalue; state.alias(lvalue, instrValue.value); lvalue.effect = Effect.Store; continuation = {kind: 'funeffects'}; break; } case 'ComputedDelete': { state.referenceAndRecordEffects( freezeActions, instrValue.object, Effect.Mutate, ValueReason.Other, ); state.referenceAndRecordEffects( freezeActions, instrValue.property, Effect.Read, ValueReason.Other, ); state.initialize(instrValue, { kind: ValueKind.Primitive, reason: new Set([ValueReason.Other]), context: new Set(), }); state.define(instr.lvalue, instrValue); instr.lvalue.effect = Effect.Mutate; continuation = {kind: 'funeffects'}; break; } case 'ComputedLoad': { state.referenceAndRecordEffects( freezeActions, instrValue.object, Effect.Read, ValueReason.Other, ); state.referenceAndRecordEffects( freezeActions, instrValue.property, Effect.Read, ValueReason.Other, ); const lvalue = instr.lvalue; lvalue.effect = Effect.ConditionallyMutate; state.initialize(instrValue, state.kind(instrValue.object)); state.define(lvalue, instrValue); continuation = {kind: 'funeffects'}; break; } case 'Await': { state.initialize(instrValue, state.kind(instrValue.value)); /* * Awaiting a value causes it to change state (go from unresolved to resolved or error) * It also means that any side-effects which would occur as part of the promise evaluation * will occur. */ state.referenceAndRecordEffects( freezeActions, instrValue.value, Effect.ConditionallyMutate, ValueReason.Other, ); const lvalue = instr.lvalue; lvalue.effect = Effect.ConditionallyMutate; state.alias(lvalue, instrValue.value); continuation = {kind: 'funeffects'}; break; } case 'TypeCastExpression': { /* * A type cast expression has no effect at runtime, so it's equivalent to a raw * identifier: * ``` * x = (y: type) // is equivalent to... * x = y * ``` */ state.initialize(instrValue, state.kind(instrValue.value)); state.referenceAndRecordEffects( freezeActions, instrValue.value, Effect.Read, ValueReason.Other, ); const lvalue = instr.lvalue; lvalue.effect = Effect.ConditionallyMutate; state.alias(lvalue, instrValue.value); continuation = {kind: 'funeffects'}; break; } case 'StartMemoize': case 'FinishMemoize': { for (const val of eachInstructionValueOperand(instrValue)) { if (env.config.enablePreserveExistingMemoizationGuarantees) { state.referenceAndRecordEffects( freezeActions, val, Effect.Freeze, ValueReason.Other, ); } else { state.referenceAndRecordEffects( freezeActions, val, Effect.Read, ValueReason.Other, ); } } const lvalue = instr.lvalue; lvalue.effect = Effect.ConditionallyMutate; state.initialize(instrValue, { kind: ValueKind.Frozen, reason: new Set([ValueReason.Other]), context: new Set(), }); state.define(lvalue, instrValue); continuation = {kind: 'funeffects'}; break; } case 'LoadLocal': { /** * Due to backedges in the CFG, we may revisit LoadLocal lvalues * multiple times. Unlike StoreLocal which may reassign to existing * identifiers, LoadLocal always evaluates to store a new temporary. * This means that we should always model LoadLocal as a Capture effect * on the rvalue. */ const lvalue = instr.lvalue; state.referenceAndRecordEffects( freezeActions, instrValue.place, Effect.Capture, ValueReason.Other, ); lvalue.effect = Effect.ConditionallyMutate; // direct aliasing: `a = b`; state.alias(lvalue, instrValue.place); continuation = {kind: 'funeffects'}; break; } case 'LoadContext': { state.referenceAndRecordEffects( freezeActions, instrValue.place, Effect.Capture, ValueReason.Other, ); const lvalue = instr.lvalue; lvalue.effect = Effect.ConditionallyMutate; const valueKind = state.kind(instrValue.place); state.initialize(instrValue, valueKind); state.define(lvalue, instrValue); continuation = {kind: 'funeffects'}; break; } case 'DeclareLocal': { const value = UndefinedValue; state.initialize( value, // Catch params may be aliased to mutable values instrValue.lvalue.kind === InstructionKind.Catch ? { kind: ValueKind.Mutable, reason: new Set([ValueReason.Other]), context: new Set(), } : { kind: ValueKind.Primitive, reason: new Set([ValueReason.Other]), context: new Set(), }, ); state.define(instrValue.lvalue.place, value); continuation = {kind: 'funeffects'}; break; } case 'DeclareContext': { state.initialize(instrValue, { kind: ValueKind.Mutable, reason: new Set([ValueReason.Other]), context: new Set(), }); state.define(instrValue.lvalue.place, instrValue); continuation = {kind: 'funeffects'}; break; } case 'PostfixUpdate': case 'PrefixUpdate': { const effect = state.isDefined(instrValue.lvalue) && state.kind(instrValue.lvalue).kind === ValueKind.Context ? Effect.ConditionallyMutate : Effect.Capture; state.referenceAndRecordEffects( freezeActions, instrValue.value, effect, ValueReason.Other, ); const lvalue = instr.lvalue; state.alias(lvalue, instrValue.value); lvalue.effect = Effect.Store; state.alias(instrValue.lvalue, instrValue.value); /* * NOTE: *not* using state.reference since this is an assignment. * reference() checks if the effect is valid given the value kind, * but here the previous value kind doesn't matter since we are * replacing it */ instrValue.lvalue.effect = Effect.Store; continuation = {kind: 'funeffects'}; break; } case 'StoreLocal': { const effect = state.isDefined(instrValue.lvalue.place) && state.kind(instrValue.lvalue.place).kind === ValueKind.Context ? Effect.ConditionallyMutate : Effect.Capture; state.referenceAndRecordEffects( freezeActions, instrValue.value, effect, ValueReason.Other, ); const lvalue = instr.lvalue; state.alias(lvalue, instrValue.value); lvalue.effect = Effect.Store; state.alias(instrValue.lvalue.place, instrValue.value); /* * NOTE: *not* using state.reference since this is an assignment. * reference() checks if the effect is valid given the value kind, * but here the previous value kind doesn't matter since we are * replacing it */ instrValue.lvalue.place.effect = Effect.Store; continuation = {kind: 'funeffects'}; break; } case 'StoreContext': { state.referenceAndRecordEffects( freezeActions, instrValue.value, Effect.ConditionallyMutate, ValueReason.Other, ); state.referenceAndRecordEffects( freezeActions, instrValue.lvalue.place, Effect.Mutate, ValueReason.Other, ); const lvalue = instr.lvalue; if (instrValue.lvalue.kind !== InstructionKind.Reassign) { state.initialize(instrValue, { kind: ValueKind.Mutable, reason: new Set([ValueReason.Other]), context: new Set(), }); state.define(instrValue.lvalue.place, instrValue); } state.alias(lvalue, instrValue.value); lvalue.effect = Effect.Store; continuation = {kind: 'funeffects'}; break; } case 'StoreGlobal': { state.referenceAndRecordEffects( freezeActions, instrValue.value, Effect.Capture, ValueReason.Other, ); const lvalue = instr.lvalue; lvalue.effect = Effect.Store; continuation = {kind: 'funeffects'}; break; } case 'Destructure': { let effect: Effect = Effect.Capture; for (const place of eachPatternOperand(instrValue.lvalue.pattern)) { if ( state.isDefined(place) && state.kind(place).kind === ValueKind.Context ) { effect = Effect.ConditionallyMutate; break; } } state.referenceAndRecordEffects( freezeActions, instrValue.value, effect, ValueReason.Other, ); const lvalue = instr.lvalue; state.alias(lvalue, instrValue.value); lvalue.effect = Effect.Store; for (const place of eachPatternOperand(instrValue.lvalue.pattern)) { state.alias(place, instrValue.value); /* * NOTE: *not* using state.reference since this is an assignment. * reference() checks if the effect is valid given the value kind, * but here the previous value kind doesn't matter since we are * replacing it */ place.effect = Effect.Store; } continuation = {kind: 'funeffects'}; break; } case 'GetIterator': { /** * This instruction represents the step of retrieving an iterator from the collection * in `for (... of )` syntax. We model two cases: * * 1. The collection is immutable or a known collection type (e.g. Array). In this case * we infer that the iterator produced won't be the same as the collection itself. * If the collection is an Array, this is because it will produce a native Array * iterator. If the collection is already frozen, we assume it must be of some * type that returns a separate iterator. In theory you could pass an Iterator * as props to a component and then for..of over that in the component body, but * this already violates React's rules so we assume you're not doing this. * 2. The collection could be an Iterator itself, such that advancing the iterator * (modeled with IteratorNext) mutates the collection itself. */ const kind = state.kind(instrValue.collection).kind; const isMutable = kind === ValueKind.Mutable || kind === ValueKind.Context; let effect; let valueKind: AbstractValue; const iterator = instrValue.collection.identifier; if ( !isMutable || isArrayType(iterator) || isMapType(iterator) || isSetType(iterator) ) { // Case 1, assume iterator is a separate mutable object effect = { kind: Effect.Read, reason: ValueReason.Other, }; valueKind = { kind: ValueKind.Mutable, reason: new Set([ValueReason.Other]), context: new Set(), }; } else { // Case 2, assume that the iterator could be the (mutable) collection itself effect = { kind: Effect.Capture, reason: ValueReason.Other, }; valueKind = state.kind(instrValue.collection); } continuation = { kind: 'initialize', effect, valueKind, lvalueEffect: Effect.Store, }; break; } case 'IteratorNext': { /** * This instruction represents advancing an iterator with .next(). We use a * conditional mutate to model the two cases for GetIterator: * - If the collection is a mutable iterator, we want to model the fact that * advancing the iterator will mutate it * - If the iterator may be different from the collection and the collection * is frozen, we don't want to report a false positive "cannot mutate" error. * * ConditionallyMutate reflects this "mutate if mutable" semantic. */ state.referenceAndRecordEffects( freezeActions, instrValue.iterator, Effect.ConditionallyMutateIterator, ValueReason.Other, ); /** * Regardless of the effect on the iterator, the *result* of advancing the iterator * is to extract a value from the collection. We use a Capture effect to reflect this * aliasing, and then initialize() the lvalue to the same kind as the colleciton to * ensure that the item is mutable or frozen if the collection is mutable/frozen. */ state.referenceAndRecordEffects( freezeActions, instrValue.collection, Effect.Capture, ValueReason.Other, ); state.initialize(instrValue, state.kind(instrValue.collection)); state.define(instr.lvalue, instrValue); instr.lvalue.effect = Effect.Store; continuation = {kind: 'funeffects'}; break; } case 'NextPropertyOf': { continuation = { kind: 'initialize', effect: {kind: Effect.Read, reason: ValueReason.Other}, lvalueEffect: Effect.Store, valueKind: { kind: ValueKind.Primitive, reason: new Set([ValueReason.Other]), context: new Set(), }, }; break; } default: { assertExhaustive(instrValue, 'Unexpected instruction kind'); } } if (continuation.kind === 'initialize') { for (const operand of eachInstructionOperand(instr)) { CompilerError.invariant(continuation.effect != null, { reason: `effectKind must be set for instruction value \`${instrValue.kind}\``, description: null, loc: instrValue.loc, suggestions: null, }); state.referenceAndRecordEffects( freezeActions, operand, continuation.effect.kind, continuation.effect.reason, ); } state.initialize(instrValue, continuation.valueKind); state.define(instr.lvalue, instrValue); instr.lvalue.effect = continuation.lvalueEffect ?? defaultLvalueEffect; } functionEffects.push(...inferInstructionFunctionEffects(env, state, instr)); freezeActions.forEach(({values, reason}) => state.freezeValues(values, reason), ); } const terminalFreezeActions: Array = []; for (const operand of eachTerminalOperand(block.terminal)) { let effect; if (block.terminal.kind === 'return' || block.terminal.kind === 'throw') { if ( state.isDefined(operand) && ((operand.identifier.type.kind === 'Function' && state.isFunctionExpression) || state.kind(operand).kind === ValueKind.Context) ) { /** * Returned values should only be typed as 'frozen' if they are both (1) * local and (2) not a function expression which may capture and mutate * this function's outer context. */ effect = Effect.ConditionallyMutate; } else { effect = Effect.Freeze; } } else { effect = Effect.Read; } state.referenceAndRecordEffects( terminalFreezeActions, operand, effect, ValueReason.Other, ); } functionEffects.push(...inferTerminalFunctionEffects(state, block)); terminalFreezeActions.forEach(({values, reason}) => state.freezeValues(values, reason), ); } function getContextRefOperand( state: InferenceState, instrValue: InstructionValue, ): Array { const result = []; for (const place of eachInstructionValueOperand(instrValue)) { if ( state.isDefined(place) && state.kind(place).kind === ValueKind.Context ) { result.push(place); } } return result; } export function getFunctionCallSignature( env: Environment, type: Type, ): FunctionSignature | null { if (type.kind !== 'Function') { return null; } return env.getFunctionSignature(type); } /* * Make a best attempt at matching arguments of a {@link MethodCall} to parameter effects. * defined in its {@link FunctionSignature}. * * @param fn * @param sig * @returns Inferred effects of function arguments, or null if inference fails. */ export function getFunctionEffects( fn: MethodCall | CallExpression | NewExpression, sig: FunctionSignature, ): Array | null { const results = []; for (let i = 0; i < fn.args.length; i++) { const arg = fn.args[i]; if (i < sig.positionalParams.length) { /* * Only infer effects when there is a direct mapping positional arg --> positional param * Otherwise, return null to indicate inference failed */ if (arg.kind === 'Identifier') { results.push(sig.positionalParams[i]); } else { return null; } } else if (sig.restParam !== null) { results.push(sig.restParam); } else { /* * If there are more arguments than positional arguments and a rest parameter is not * defined, we'll also assume that inference failed */ return null; } } return results; } export function isKnownMutableEffect(effect: Effect): boolean { switch (effect) { case Effect.Store: case Effect.ConditionallyMutate: case Effect.ConditionallyMutateIterator: case Effect.Mutate: { return true; } case Effect.Unknown: { CompilerError.invariant(false, { reason: 'Unexpected unknown effect', description: null, loc: GeneratedSource, suggestions: null, }); } case Effect.Read: case Effect.Capture: case Effect.Freeze: { return false; } default: { assertExhaustive(effect, `Unexpected effect \`${effect}\``); } } } /** * Returns true if all of the arguments are both non-mutable (immutable or frozen) * _and_ are not functions which might mutate their arguments. Note that function * expressions count as frozen so long as they do not mutate free variables: this * function checks that such functions also don't mutate their inputs. */ function areArgumentsImmutableAndNonMutating( state: InferenceState, args: MethodCall['args'], ): boolean { for (const arg of args) { if (arg.kind === 'Identifier' && arg.identifier.type.kind === 'Function') { const fnShape = state.env.getFunctionSignature(arg.identifier.type); if (fnShape != null) { return ( !fnShape.positionalParams.some(isKnownMutableEffect) && (fnShape.restParam == null || !isKnownMutableEffect(fnShape.restParam)) ); } } const place = arg.kind === 'Identifier' ? arg : arg.place; const kind = state.kind(place).kind; switch (kind) { case ValueKind.Primitive: case ValueKind.Frozen: { /* * Only immutable values, or frozen lambdas are allowed. * A lambda may appear frozen even if it may mutate its inputs, * so we have a second check even for frozen value types */ break; } default: { /** * Globals, module locals, and other locally defined functions may * mutate their arguments. */ return false; } } const values = state.values(place); for (const value of values) { if ( value.kind === 'FunctionExpression' && value.loweredFunc.func.params.some(param => { const place = param.kind === 'Identifier' ? param : param.place; const range = place.identifier.mutableRange; return range.end > range.start + 1; }) ) { // This is a function which may mutate its inputs return false; } } } return true; } function getArgumentEffect( signatureEffect: Effect | null, arg: Place | SpreadPattern, ): Effect { if (signatureEffect != null) { if (arg.kind === 'Identifier') { return signatureEffect; } else if ( signatureEffect === Effect.Mutate || signatureEffect === Effect.ConditionallyMutate ) { return signatureEffect; } else { // see call-spread-argument-mutable-iterator test fixture if (signatureEffect === Effect.Freeze) { CompilerError.throwTodo({ reason: 'Support spread syntax for hook arguments', loc: arg.place.loc, }); } // effects[i] is Effect.Capture | Effect.Read | Effect.Store return Effect.ConditionallyMutateIterator; } } else { return Effect.ConditionallyMutate; } } function inferCallEffects( state: InferenceState, instr: | TInstruction | TInstruction | TInstruction, freezeActions: Array, signature: FunctionSignature | null, ): void { const instrValue = instr.value; const returnValueKind: AbstractValue = signature !== null ? { kind: signature.returnValueKind, reason: new Set([ signature.returnValueReason ?? ValueReason.KnownReturnSignature, ]), context: new Set(), } : { kind: ValueKind.Mutable, reason: new Set([ValueReason.Other]), context: new Set(), }; if ( instrValue.kind === 'MethodCall' && signature !== null && signature.mutableOnlyIfOperandsAreMutable && areArgumentsImmutableAndNonMutating(state, instrValue.args) ) { /* * None of the args are mutable or mutate their params, we can downgrade to * treating as all reads (except that the receiver may be captured) */ for (const arg of instrValue.args) { const place = arg.kind === 'Identifier' ? arg : arg.place; state.referenceAndRecordEffects( freezeActions, place, Effect.Read, ValueReason.Other, ); } state.referenceAndRecordEffects( freezeActions, instrValue.receiver, Effect.Capture, ValueReason.Other, ); state.initialize(instrValue, returnValueKind); state.define(instr.lvalue, instrValue); instr.lvalue.effect = instrValue.receiver.effect === Effect.Capture ? Effect.Store : Effect.ConditionallyMutate; return; } const effects = signature !== null ? getFunctionEffects(instrValue, signature) : null; let hasCaptureArgument = false; for (let i = 0; i < instrValue.args.length; i++) { const arg = instrValue.args[i]; const place = arg.kind === 'Identifier' ? arg : arg.place; /* * If effects are inferred for an argument, we should fail invalid * mutating effects */ state.referenceAndRecordEffects( freezeActions, place, getArgumentEffect(effects != null ? effects[i] : null, arg), ValueReason.Other, ); hasCaptureArgument ||= place.effect === Effect.Capture; } const callee = instrValue.kind === 'MethodCall' ? instrValue.receiver : instrValue.callee; if (signature !== null) { state.referenceAndRecordEffects( freezeActions, callee, signature.calleeEffect, ValueReason.Other, ); } else { /** * For new expressions, we infer a `read` effect on the Class / Function type * to avoid extending mutable ranges of locally created classes, e.g. * ```js * const MyClass = getClass(); * const value = new MyClass(val1, val2) * ^ (read) ^ (conditionally mutate) * ``` * * Risks: * Classes / functions created during render could technically capture and * mutate their enclosing scope, which we currently do not detect. */ state.referenceAndRecordEffects( freezeActions, callee, instrValue.kind === 'NewExpression' ? Effect.Read : Effect.ConditionallyMutate, ValueReason.Other, ); } hasCaptureArgument ||= callee.effect === Effect.Capture; state.initialize(instrValue, returnValueKind); state.define(instr.lvalue, instrValue); instr.lvalue.effect = hasCaptureArgument ? Effect.Store : Effect.ConditionallyMutate; }