mirror of
https://github.com/facebook/react.git
synced 2026-02-22 03:42:05 +00:00
[compiler] New mutability/aliasing model (#33494)
Squashed, review-friendly version of the stack from https://github.com/facebook/react/pull/33488. This is new version of our mutability and inference model, designed to replace the core algorithm for determining the sets of instructions involved in constructing a given value or set of values. The new model replaces InferReferenceEffects, InferMutableRanges (and all of its subcomponents), and parts of AnalyzeFunctions. The new model does not use per-Place effect values, but in order to make this drop-in the end _result_ of the inference adds these per-Place effects. I'll write up a larger document on the model, first i'm doing some housekeeping to rebase the PR. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33494). * #33571 * #33558 * #33547 * #33543 * #33533 * #33532 * #33530 * #33526 * #33522 * #33518 * #33514 * #33513 * #33512 * #33504 * #33500 * #33497 * #33496 * #33495 * __->__ #33494 * #33572
This commit is contained in:
@@ -104,6 +104,8 @@ import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureF
|
||||
import {CompilerError} from '..';
|
||||
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
|
||||
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
|
||||
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
|
||||
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
| {kind: 'ast'; name: string; value: CodegenFunction}
|
||||
@@ -227,15 +229,27 @@ function runWithEnvironment(
|
||||
analyseFunctions(hir);
|
||||
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
|
||||
|
||||
const fnEffectErrors = inferReferenceEffects(hir);
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (fnEffectErrors.length > 0) {
|
||||
CompilerError.throw(fnEffectErrors[0]);
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
const fnEffectErrors = inferReferenceEffects(hir);
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (fnEffectErrors.length > 0) {
|
||||
CompilerError.throw(fnEffectErrors[0]);
|
||||
}
|
||||
}
|
||||
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
|
||||
} else {
|
||||
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
|
||||
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
}
|
||||
}
|
||||
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
|
||||
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
|
||||
// Note: Has to come after infer reference effects because "dead" code may still affect inference
|
||||
deadCodeElimination(hir);
|
||||
@@ -249,8 +263,21 @@ function runWithEnvironment(
|
||||
pruneMaybeThrows(hir);
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
inferMutableRanges(hir);
|
||||
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
inferMutableRanges(hir);
|
||||
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
|
||||
} else {
|
||||
const mutabilityAliasingErrors = inferMutationAliasingRanges(hir, {
|
||||
isFunctionExpression: false,
|
||||
});
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
}
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.config.assertValidMutableRanges) {
|
||||
@@ -277,7 +304,10 @@ function runWithEnvironment(
|
||||
validateNoImpureFunctionsInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (env.config.validateNoFreezingKnownMutableFunctions) {
|
||||
if (
|
||||
env.config.validateNoFreezingKnownMutableFunctions ||
|
||||
env.config.enableNewMutationAliasingModel
|
||||
) {
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import invariant from 'invariant';
|
||||
import {HIRFunction, Identifier, MutableRange} from './HIR';
|
||||
import {HIRFunction, MutableRange, Place} from './HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionOperand,
|
||||
eachTerminalOperand,
|
||||
} from './visitors';
|
||||
import {CompilerError} from '..';
|
||||
import {printPlace} from './PrintHIR';
|
||||
|
||||
/*
|
||||
* Checks that all mutable ranges in the function are well-formed, with
|
||||
@@ -20,38 +21,43 @@ import {
|
||||
export function assertValidMutableRanges(fn: HIRFunction): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
visitIdentifier(phi.place.identifier);
|
||||
for (const [, operand] of phi.operands) {
|
||||
visitIdentifier(operand.identifier);
|
||||
visit(phi.place, `phi for block bb${block.id}`);
|
||||
for (const [pred, operand] of phi.operands) {
|
||||
visit(operand, `phi predecessor bb${pred} for block bb${block.id}`);
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
visitIdentifier(operand.identifier);
|
||||
visit(operand, `instruction [${instr.id}]`);
|
||||
}
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
visitIdentifier(operand.identifier);
|
||||
visit(operand, `instruction [${instr.id}]`);
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
visitIdentifier(operand.identifier);
|
||||
visit(operand, `terminal [${block.terminal.id}]`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function visitIdentifier(identifier: Identifier): void {
|
||||
validateMutableRange(identifier.mutableRange);
|
||||
if (identifier.scope !== null) {
|
||||
validateMutableRange(identifier.scope.range);
|
||||
function visit(place: Place, description: string): void {
|
||||
validateMutableRange(place, place.identifier.mutableRange, description);
|
||||
if (place.identifier.scope !== null) {
|
||||
validateMutableRange(place, place.identifier.scope.range, description);
|
||||
}
|
||||
}
|
||||
|
||||
function validateMutableRange(mutableRange: MutableRange): void {
|
||||
invariant(
|
||||
(mutableRange.start === 0 && mutableRange.end === 0) ||
|
||||
mutableRange.end > mutableRange.start,
|
||||
'Identifier scope mutableRange was invalid: [%s:%s]',
|
||||
mutableRange.start,
|
||||
mutableRange.end,
|
||||
function validateMutableRange(
|
||||
place: Place,
|
||||
range: MutableRange,
|
||||
description: string,
|
||||
): void {
|
||||
CompilerError.invariant(
|
||||
(range.start === 0 && range.end === 0) || range.end > range.start,
|
||||
{
|
||||
reason: `Invalid mutable range: [${range.start}:${range.end}]`,
|
||||
description: `${printPlace(place)} in ${description}`,
|
||||
loc: place.loc,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
makeType,
|
||||
promoteTemporary,
|
||||
} from './HIR';
|
||||
import HIRBuilder, {Bindings} from './HIRBuilder';
|
||||
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
|
||||
import {BuiltInArrayId} from './ObjectShape';
|
||||
|
||||
/*
|
||||
@@ -181,6 +181,7 @@ export function lower(
|
||||
loc: GeneratedSource,
|
||||
value: lowerExpressionToTemporary(builder, body),
|
||||
id: makeInstructionId(0),
|
||||
effects: null,
|
||||
};
|
||||
builder.terminateWithContinuation(terminal, fallthrough);
|
||||
} else if (body.isBlockStatement()) {
|
||||
@@ -210,6 +211,7 @@ export function lower(
|
||||
loc: GeneratedSource,
|
||||
}),
|
||||
id: makeInstructionId(0),
|
||||
effects: null,
|
||||
},
|
||||
null,
|
||||
);
|
||||
@@ -220,6 +222,7 @@ export function lower(
|
||||
fnType: bindings == null ? env.fnType : 'Other',
|
||||
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
|
||||
returnType: makeType(),
|
||||
returns: createTemporaryPlace(env, func.node.loc ?? GeneratedSource),
|
||||
body: builder.build(),
|
||||
context,
|
||||
generator: func.node.generator === true,
|
||||
@@ -227,6 +230,7 @@ export function lower(
|
||||
loc: func.node.loc ?? GeneratedSource,
|
||||
env,
|
||||
effects: null,
|
||||
aliasingEffects: null,
|
||||
directives,
|
||||
});
|
||||
}
|
||||
@@ -287,6 +291,7 @@ function lowerStatement(
|
||||
loc: stmt.node.loc ?? GeneratedSource,
|
||||
value,
|
||||
id: makeInstructionId(0),
|
||||
effects: null,
|
||||
};
|
||||
builder.terminate(terminal, 'block');
|
||||
return;
|
||||
@@ -1237,6 +1242,7 @@ function lowerStatement(
|
||||
kind: 'Debugger',
|
||||
loc,
|
||||
},
|
||||
effects: null,
|
||||
loc,
|
||||
});
|
||||
return;
|
||||
@@ -1894,6 +1900,7 @@ function lowerExpression(
|
||||
place: leftValue,
|
||||
loc: exprLoc,
|
||||
},
|
||||
effects: null,
|
||||
loc: exprLoc,
|
||||
});
|
||||
builder.terminateWithContinuation(
|
||||
@@ -2829,6 +2836,7 @@ function lowerOptionalCallExpression(
|
||||
args,
|
||||
loc,
|
||||
},
|
||||
effects: null,
|
||||
loc,
|
||||
});
|
||||
} else {
|
||||
@@ -2842,6 +2850,7 @@ function lowerOptionalCallExpression(
|
||||
args,
|
||||
loc,
|
||||
},
|
||||
effects: null,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
@@ -3465,9 +3474,10 @@ export function lowerValueToTemporary(
|
||||
const place: Place = buildTemporaryPlace(builder, value.loc);
|
||||
builder.push({
|
||||
id: makeInstructionId(0),
|
||||
value: value,
|
||||
loc: value.loc,
|
||||
lvalue: {...place},
|
||||
value: value,
|
||||
effects: null,
|
||||
loc: value.loc,
|
||||
});
|
||||
return place;
|
||||
}
|
||||
|
||||
@@ -243,6 +243,11 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
enableUseTypeAnnotations: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Enable a new model for mutability and aliasing inference
|
||||
*/
|
||||
enableNewMutationAliasingModel: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Enables inference of optional dependency chains. Without this flag
|
||||
* a property chain such as `props?.items?.foo` will infer as a dep on
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {Effect, ValueKind, ValueReason} from './HIR';
|
||||
import {Effect, makeIdentifierId, ValueKind, ValueReason} from './HIR';
|
||||
import {
|
||||
BUILTIN_SHAPES,
|
||||
BuiltInArrayId,
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
addFunction,
|
||||
addHook,
|
||||
addObject,
|
||||
signatureArgument,
|
||||
} from './ObjectShape';
|
||||
import {BuiltInType, ObjectType, PolyType} from './Types';
|
||||
import {TypeConfig} from './TypeSchema';
|
||||
@@ -644,6 +645,41 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useEffect',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
aliasing: {
|
||||
receiver: makeIdentifierId(0),
|
||||
params: [],
|
||||
rest: makeIdentifierId(1),
|
||||
returns: makeIdentifierId(2),
|
||||
temporaries: [signatureArgument(3)],
|
||||
effects: [
|
||||
// Freezes the function and deps
|
||||
{
|
||||
kind: 'Freeze',
|
||||
value: signatureArgument(1),
|
||||
reason: ValueReason.Effect,
|
||||
},
|
||||
// Internally creates an effect object that captures the function and deps
|
||||
{
|
||||
kind: 'Create',
|
||||
into: signatureArgument(3),
|
||||
value: ValueKind.Frozen,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
// The effect stores the function and dependencies
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: signatureArgument(1),
|
||||
into: signatureArgument(3),
|
||||
},
|
||||
// Returns undefined
|
||||
{
|
||||
kind: 'Create',
|
||||
into: signatureArgument(2),
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
BuiltInUseEffectHookId,
|
||||
),
|
||||
|
||||
@@ -13,6 +13,7 @@ import {Environment, ReactFunctionType} from './Environment';
|
||||
import type {HookKind} from './ObjectShape';
|
||||
import {Type, makeType} from './Types';
|
||||
import {z} from 'zod';
|
||||
import type {AliasingEffect} from '../Inference/AliasingEffects';
|
||||
|
||||
/*
|
||||
* *******************************************************************************************
|
||||
@@ -100,6 +101,7 @@ export type ReactiveInstruction = {
|
||||
id: InstructionId;
|
||||
lvalue: Place | null;
|
||||
value: ReactiveValue;
|
||||
effects?: Array<AliasingEffect> | null; // TODO make non-optional
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
@@ -278,12 +280,14 @@ export type HIRFunction = {
|
||||
params: Array<Place | SpreadPattern>;
|
||||
returnTypeAnnotation: t.FlowType | t.TSType | null;
|
||||
returnType: Type;
|
||||
returns: Place;
|
||||
context: Array<Place>;
|
||||
effects: Array<FunctionEffect> | null;
|
||||
body: HIR;
|
||||
generator: boolean;
|
||||
async: boolean;
|
||||
directives: Array<string>;
|
||||
aliasingEffects?: Array<AliasingEffect> | null;
|
||||
};
|
||||
|
||||
export type FunctionEffect =
|
||||
@@ -449,6 +453,7 @@ export type ReturnTerminal = {
|
||||
value: Place;
|
||||
id: InstructionId;
|
||||
fallthrough?: never;
|
||||
effects: Array<AliasingEffect> | null;
|
||||
};
|
||||
|
||||
export type GotoTerminal = {
|
||||
@@ -609,6 +614,7 @@ export type MaybeThrowTerminal = {
|
||||
id: InstructionId;
|
||||
loc: SourceLocation;
|
||||
fallthrough?: never;
|
||||
effects: Array<AliasingEffect> | null;
|
||||
};
|
||||
|
||||
export type ReactiveScopeTerminal = {
|
||||
@@ -645,12 +651,14 @@ export type Instruction = {
|
||||
lvalue: Place;
|
||||
value: InstructionValue;
|
||||
loc: SourceLocation;
|
||||
effects: Array<AliasingEffect> | null;
|
||||
};
|
||||
|
||||
export type TInstruction<T extends InstructionValue> = {
|
||||
id: InstructionId;
|
||||
lvalue: Place;
|
||||
value: T;
|
||||
effects: Array<AliasingEffect> | null;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
@@ -1380,6 +1388,11 @@ export enum ValueReason {
|
||||
*/
|
||||
JsxCaptured = 'jsx-captured',
|
||||
|
||||
/**
|
||||
* Passed to an effect
|
||||
*/
|
||||
Effect = 'effect',
|
||||
|
||||
/**
|
||||
* Return value of a function with known frozen return value, e.g. `useState`.
|
||||
*/
|
||||
|
||||
@@ -165,6 +165,7 @@ export default class HIRBuilder {
|
||||
handler: exceptionHandler,
|
||||
id: makeInstructionId(0),
|
||||
loc: instruction.loc,
|
||||
effects: null,
|
||||
},
|
||||
continuationBlock,
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
Instruction,
|
||||
Place,
|
||||
} from './HIR';
|
||||
import {markPredecessors} from './HIRBuilder';
|
||||
import {terminalFallthrough, terminalHasFallthrough} from './visitors';
|
||||
@@ -80,20 +81,22 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
|
||||
suggestions: null,
|
||||
});
|
||||
const operand = Array.from(phi.operands.values())[0]!;
|
||||
const lvalue: Place = {
|
||||
kind: 'Identifier',
|
||||
identifier: phi.place.identifier,
|
||||
effect: Effect.ConditionallyMutate,
|
||||
reactive: false,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
const instr: Instruction = {
|
||||
id: predecessor.terminal.id,
|
||||
lvalue: {
|
||||
kind: 'Identifier',
|
||||
identifier: phi.place.identifier,
|
||||
effect: Effect.ConditionallyMutate,
|
||||
reactive: false,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
lvalue: {...lvalue},
|
||||
value: {
|
||||
kind: 'LoadLocal',
|
||||
place: {...operand},
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
effects: [{kind: 'Alias', from: {...operand}, into: {...lvalue}}],
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
predecessor.instructions.push(instr);
|
||||
|
||||
@@ -6,10 +6,21 @@
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Effect, ValueKind, ValueReason} from './HIR';
|
||||
import {AliasingSignature} from '../Inference/AliasingEffects';
|
||||
import {
|
||||
Effect,
|
||||
GeneratedSource,
|
||||
makeDeclarationId,
|
||||
makeIdentifierId,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
ValueKind,
|
||||
ValueReason,
|
||||
} from './HIR';
|
||||
import {
|
||||
BuiltInType,
|
||||
FunctionType,
|
||||
makeType,
|
||||
ObjectType,
|
||||
PolyType,
|
||||
PrimitiveType,
|
||||
@@ -180,6 +191,9 @@ export type FunctionSignature = {
|
||||
impure?: boolean;
|
||||
|
||||
canonicalName?: string;
|
||||
|
||||
aliasing?: AliasingSignature | null;
|
||||
todo_aliasing?: AliasingSignature | null;
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -305,6 +319,30 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Store,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
aliasing: {
|
||||
receiver: makeIdentifierId(0),
|
||||
params: [],
|
||||
rest: makeIdentifierId(1),
|
||||
returns: makeIdentifierId(2),
|
||||
temporaries: [],
|
||||
effects: [
|
||||
// Push directly mutates the array itself
|
||||
{kind: 'Mutate', value: signatureArgument(0)},
|
||||
// The arguments are captured into the array
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: signatureArgument(1),
|
||||
into: signatureArgument(0),
|
||||
},
|
||||
// Returns the new length, a primitive
|
||||
{
|
||||
kind: 'Create',
|
||||
into: signatureArgument(2),
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -335,6 +373,62 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
noAlias: true,
|
||||
mutableOnlyIfOperandsAreMutable: true,
|
||||
aliasing: {
|
||||
receiver: makeIdentifierId(0),
|
||||
params: [makeIdentifierId(1)],
|
||||
rest: null,
|
||||
returns: makeIdentifierId(2),
|
||||
temporaries: [
|
||||
// Temporary representing captured items of the receiver
|
||||
signatureArgument(3),
|
||||
// Temporary representing the result of the callback
|
||||
signatureArgument(4),
|
||||
/*
|
||||
* Undefined `this` arg to the callback. Note the signature does not
|
||||
* support passing an explicit thisArg second param
|
||||
*/
|
||||
signatureArgument(5),
|
||||
],
|
||||
effects: [
|
||||
// Map creates a new mutable array
|
||||
{
|
||||
kind: 'Create',
|
||||
into: signatureArgument(2),
|
||||
value: ValueKind.Mutable,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
// The first arg to the callback is an item extracted from the receiver array
|
||||
{
|
||||
kind: 'CreateFrom',
|
||||
from: signatureArgument(0),
|
||||
into: signatureArgument(3),
|
||||
},
|
||||
// The undefined this for the callback
|
||||
{
|
||||
kind: 'Create',
|
||||
into: signatureArgument(5),
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
// calls the callback, returning the result into a temporary
|
||||
{
|
||||
kind: 'Apply',
|
||||
receiver: signatureArgument(5),
|
||||
args: [signatureArgument(3), {kind: 'Hole'}, signatureArgument(0)],
|
||||
function: signatureArgument(1),
|
||||
into: signatureArgument(4),
|
||||
signature: null,
|
||||
mutatesFunction: false,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
// captures the result of the callback into the return array
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: signatureArgument(4),
|
||||
into: signatureArgument(2),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -482,6 +576,32 @@ addObject(BUILTIN_SHAPES, BuiltInSetId, [
|
||||
calleeEffect: Effect.Store,
|
||||
// returnValueKind is technically dependent on the ValueKind of the set itself
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
aliasing: {
|
||||
receiver: makeIdentifierId(0),
|
||||
params: [],
|
||||
rest: makeIdentifierId(1),
|
||||
returns: makeIdentifierId(2),
|
||||
temporaries: [],
|
||||
effects: [
|
||||
// Set.add returns the receiver Set
|
||||
{
|
||||
kind: 'Assign',
|
||||
from: signatureArgument(0),
|
||||
into: signatureArgument(2),
|
||||
},
|
||||
// Set.add mutates the set itself
|
||||
{
|
||||
kind: 'Mutate',
|
||||
value: signatureArgument(0),
|
||||
},
|
||||
// Captures the rest params into the set
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: signatureArgument(1),
|
||||
into: signatureArgument(0),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -1185,3 +1305,22 @@ export const DefaultNonmutatingHook = addHook(
|
||||
},
|
||||
'DefaultNonmutatingHook',
|
||||
);
|
||||
|
||||
export function signatureArgument(id: number): Place {
|
||||
const place: Place = {
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Unknown,
|
||||
loc: GeneratedSource,
|
||||
reactive: false,
|
||||
identifier: {
|
||||
declarationId: makeDeclarationId(id),
|
||||
id: makeIdentifierId(id),
|
||||
loc: GeneratedSource,
|
||||
mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)},
|
||||
name: null,
|
||||
scope: null,
|
||||
type: makeType(),
|
||||
},
|
||||
};
|
||||
return place;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import type {
|
||||
Type,
|
||||
} from './HIR';
|
||||
import {GotoVariant, InstructionKind} from './HIR';
|
||||
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
|
||||
|
||||
export type Options = {
|
||||
indent: number;
|
||||
@@ -67,13 +68,15 @@ export function printFunction(fn: HIRFunction): string {
|
||||
})
|
||||
.join(', ') +
|
||||
')';
|
||||
} else {
|
||||
definition += '()';
|
||||
}
|
||||
if (definition.length !== 0) {
|
||||
output.push(definition);
|
||||
}
|
||||
output.push(printType(fn.returnType));
|
||||
output.push(printHIR(fn.body));
|
||||
output.push(`: ${printType(fn.returnType)} @ ${printPlace(fn.returns)}`);
|
||||
output.push(...fn.directives);
|
||||
output.push(printHIR(fn.body));
|
||||
return output.join('\n');
|
||||
}
|
||||
|
||||
@@ -151,7 +154,10 @@ export function printMixedHIR(
|
||||
|
||||
export function printInstruction(instr: ReactiveInstruction): string {
|
||||
const id = `[${instr.id}]`;
|
||||
const value = printInstructionValue(instr.value);
|
||||
let value = printInstructionValue(instr.value);
|
||||
if (instr.effects != null) {
|
||||
value += `\n ${instr.effects.map(printAliasingEffect).join('\n ')}`;
|
||||
}
|
||||
|
||||
if (instr.lvalue !== null) {
|
||||
return `${id} ${printPlace(instr.lvalue)} = ${value}`;
|
||||
@@ -213,6 +219,9 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
|
||||
value = `[${terminal.id}] Return${
|
||||
terminal.value != null ? ' ' + printPlace(terminal.value) : ''
|
||||
}`;
|
||||
if (terminal.effects != null) {
|
||||
value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'goto': {
|
||||
@@ -281,6 +290,9 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
|
||||
}
|
||||
case 'maybe-throw': {
|
||||
value = `[${terminal.id}] MaybeThrow continuation=bb${terminal.continuation} handler=bb${terminal.handler}`;
|
||||
if (terminal.effects != null) {
|
||||
value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'scope': {
|
||||
@@ -555,8 +567,11 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
}
|
||||
})
|
||||
.join(', ') ?? '';
|
||||
const type = printType(instrValue.loweredFunc.func.returnType).trim();
|
||||
value = `${kind} ${name} @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`;
|
||||
const aliasingEffects =
|
||||
instrValue.loweredFunc.func.aliasingEffects
|
||||
?.map(printAliasingEffect)
|
||||
?.join(', ') ?? '';
|
||||
value = `${kind} ${name} @context[${context}] @effects[${effects}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
|
||||
break;
|
||||
}
|
||||
case 'TaggedTemplateExpression': {
|
||||
@@ -922,3 +937,107 @@ function getFunctionName(
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
export function printAliasingEffect(effect: AliasingEffect): string {
|
||||
switch (effect.kind) {
|
||||
case 'Assign': {
|
||||
return `Assign ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'Alias': {
|
||||
return `Alias ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'Capture': {
|
||||
return `Capture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'ImmutableCapture': {
|
||||
return `ImmutableCapture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'Create': {
|
||||
return `Create ${printPlaceForAliasEffect(effect.into)} = ${effect.value}`;
|
||||
}
|
||||
case 'CreateFrom': {
|
||||
return `Create ${printPlaceForAliasEffect(effect.into)} = kindOf(${printPlaceForAliasEffect(effect.from)})`;
|
||||
}
|
||||
case 'CreateFunction': {
|
||||
return `Function ${printPlaceForAliasEffect(effect.into)} = Function captures=[${effect.captures.map(printPlaceForAliasEffect).join(', ')}]`;
|
||||
}
|
||||
case 'Apply': {
|
||||
const receiverCallee =
|
||||
effect.receiver.identifier.id === effect.function.identifier.id
|
||||
? printPlaceForAliasEffect(effect.receiver)
|
||||
: `${printPlaceForAliasEffect(effect.receiver)}.${printPlaceForAliasEffect(effect.function)}`;
|
||||
const args = effect.args
|
||||
.map(arg => {
|
||||
if (arg.kind === 'Identifier') {
|
||||
return printPlaceForAliasEffect(arg);
|
||||
} else if (arg.kind === 'Hole') {
|
||||
return ' ';
|
||||
}
|
||||
return `...${printPlaceForAliasEffect(arg.place)}`;
|
||||
})
|
||||
.join(', ');
|
||||
let signature = '';
|
||||
if (effect.signature != null) {
|
||||
if (effect.signature.aliasing != null) {
|
||||
signature = printAliasingSignature(effect.signature.aliasing);
|
||||
} else {
|
||||
signature = JSON.stringify(effect.signature, null, 2);
|
||||
}
|
||||
}
|
||||
return `Apply ${printPlaceForAliasEffect(effect.into)} = ${receiverCallee}(${args})${signature != '' ? '\n ' : ''}${signature}`;
|
||||
}
|
||||
case 'Freeze': {
|
||||
return `Freeze ${printPlaceForAliasEffect(effect.value)} ${effect.reason}`;
|
||||
}
|
||||
case 'Mutate':
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitive':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
|
||||
}
|
||||
case 'MutateFrozen': {
|
||||
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'MutateGlobal': {
|
||||
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'Impure': {
|
||||
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'Render': {
|
||||
return `Render ${printPlaceForAliasEffect(effect.place)}`;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(effect, `Unexpected kind '${(effect as any).kind}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printPlaceForAliasEffect(place: Place): string {
|
||||
return printIdentifier(place.identifier);
|
||||
}
|
||||
|
||||
export function printAliasingSignature(signature: AliasingSignature): string {
|
||||
const tokens: Array<string> = ['function '];
|
||||
if (signature.temporaries.length !== 0) {
|
||||
tokens.push('<');
|
||||
tokens.push(
|
||||
signature.temporaries.map(temp => `$${temp.identifier.id}`).join(', '),
|
||||
);
|
||||
tokens.push('>');
|
||||
}
|
||||
tokens.push('(');
|
||||
tokens.push('this=$' + String(signature.receiver));
|
||||
for (const param of signature.params) {
|
||||
tokens.push(', $' + String(param));
|
||||
}
|
||||
if (signature.rest != null) {
|
||||
tokens.push(`, ...$${String(signature.rest)}`);
|
||||
}
|
||||
tokens.push('): ');
|
||||
tokens.push('$' + String(signature.returns) + ':');
|
||||
for (const effect of signature.effects) {
|
||||
tokens.push('\n ' + printAliasingEffect(effect));
|
||||
}
|
||||
return tokens.join('');
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ function writeNonOptionalDependency(
|
||||
},
|
||||
id: makeInstructionId(1),
|
||||
loc: loc,
|
||||
effects: null,
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -118,6 +119,7 @@ function writeNonOptionalDependency(
|
||||
},
|
||||
id: makeInstructionId(1),
|
||||
loc: loc,
|
||||
effects: null,
|
||||
});
|
||||
curr = next;
|
||||
}
|
||||
|
||||
@@ -735,6 +735,7 @@ export function mapTerminalSuccessors(
|
||||
loc: terminal.loc,
|
||||
value: terminal.value,
|
||||
id: makeInstructionId(0),
|
||||
effects: terminal.effects,
|
||||
};
|
||||
}
|
||||
case 'throw': {
|
||||
@@ -842,6 +843,7 @@ export function mapTerminalSuccessors(
|
||||
handler,
|
||||
id: makeInstructionId(0),
|
||||
loc: terminal.loc,
|
||||
effects: terminal.effects,
|
||||
};
|
||||
}
|
||||
case 'try': {
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* 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 {CompilerErrorDetailOptions} from '../CompilerError';
|
||||
import {
|
||||
FunctionExpression,
|
||||
Hole,
|
||||
IdentifierId,
|
||||
ObjectMethod,
|
||||
Place,
|
||||
SourceLocation,
|
||||
SpreadPattern,
|
||||
ValueKind,
|
||||
ValueReason,
|
||||
} from '../HIR';
|
||||
import {FunctionSignature} from '../HIR/ObjectShape';
|
||||
|
||||
/**
|
||||
* `AliasingEffect` describes a set of "effects" that an instruction/terminal has on one or
|
||||
* more values in a program. These effects include mutation of values, freezing values,
|
||||
* tracking data flow between values, and other specialized cases.
|
||||
*/
|
||||
export type AliasingEffect =
|
||||
/**
|
||||
* Marks the given value and its direct aliases as frozen.
|
||||
*
|
||||
* Captured values are *not* considered frozen, because we cannot be sure that a previously
|
||||
* captured value will still be captured at the point of the freeze.
|
||||
*
|
||||
* For example:
|
||||
* const x = {};
|
||||
* const y = [x];
|
||||
* y.pop(); // y dosn't contain x anymore!
|
||||
* freeze(y);
|
||||
* mutate(x); // safe to mutate!
|
||||
*
|
||||
* The exception to this is FunctionExpressions - since it is impossible to change which
|
||||
* value a function closes over[1] we can transitively freeze functions and their captures.
|
||||
*
|
||||
* [1] Except for `let` values that are reassigned and closed over by a function, but we
|
||||
* handle this explicitly with StoreContext/LoadContext.
|
||||
*/
|
||||
| {kind: 'Freeze'; value: Place; reason: ValueReason}
|
||||
/**
|
||||
* Mutate the value and any direct aliases (not captures). Errors if the value is not mutable.
|
||||
*/
|
||||
| {kind: 'Mutate'; value: Place}
|
||||
/**
|
||||
* Mutate the value and any direct aliases (not captures), but only if the value is known mutable.
|
||||
* This should be rare.
|
||||
*
|
||||
* TODO: this is only used for IteratorNext, but even then MutateTransitiveConditionally is more
|
||||
* correct for iterators of unknown types.
|
||||
*/
|
||||
| {kind: 'MutateConditionally'; value: Place}
|
||||
/**
|
||||
* Mutate the value, any direct aliases, and any transitive captures. Errors if the value is not mutable.
|
||||
*/
|
||||
| {kind: 'MutateTransitive'; value: Place}
|
||||
/**
|
||||
* Mutates any of the value, its direct aliases, and its transitive captures that are mutable.
|
||||
*/
|
||||
| {kind: 'MutateTransitiveConditionally'; value: Place}
|
||||
/**
|
||||
* Records information flow from `from` to `into` in cases where local mutation of the destination
|
||||
* will *not* mutate the source:
|
||||
*
|
||||
* - Capture a -> b and Mutate(b) X=> (does not imply) Mutate(a)
|
||||
* - Capture a -> b and MutateTransitive(b) => (does imply) Mutate(a)
|
||||
*
|
||||
* Example: `array.push(item)`. Information from item is captured into array, but there is not a
|
||||
* direct aliasing, and local mutations of array will not modify item.
|
||||
*/
|
||||
| {kind: 'Capture'; from: Place; into: Place}
|
||||
/**
|
||||
* Records information flow from `from` to `into` in cases where local mutation of the destination
|
||||
* *will* mutate the source:
|
||||
*
|
||||
* - Alias a -> b and Mutate(b) => (does imply) Mutate(a)
|
||||
* - Alias a -> b and MutateTransitive(b) => (does imply) Mutate(a)
|
||||
*
|
||||
* Example: `c = identity(a)`. We don't know what `identity()` returns so we can't use Assign.
|
||||
* But we have to assume that it _could_ be returning its input, such that a local mutation of
|
||||
* c could be mutating a.
|
||||
*/
|
||||
| {kind: 'Alias'; from: Place; into: Place}
|
||||
/**
|
||||
* Records direct assignment: `into = from`.
|
||||
*/
|
||||
| {kind: 'Assign'; from: Place; into: Place}
|
||||
/**
|
||||
* Creates a value of the given type at the given place
|
||||
*/
|
||||
| {kind: 'Create'; into: Place; value: ValueKind; reason: ValueReason}
|
||||
/**
|
||||
* Creates a new value with the same kind as the starting value.
|
||||
*/
|
||||
| {kind: 'CreateFrom'; from: Place; into: Place}
|
||||
/**
|
||||
* Immutable data flow, used for escape analysis. Does not influence mutable range analysis:
|
||||
*/
|
||||
| {kind: 'ImmutableCapture'; from: Place; into: Place}
|
||||
/**
|
||||
* Calls the function at the given place with the given arguments either captured or aliased,
|
||||
* and captures/aliases the result into the given place.
|
||||
*/
|
||||
| {
|
||||
kind: 'Apply';
|
||||
receiver: Place;
|
||||
function: Place;
|
||||
mutatesFunction: boolean;
|
||||
args: Array<Place | SpreadPattern | Hole>;
|
||||
into: Place;
|
||||
signature: FunctionSignature | null;
|
||||
loc: SourceLocation;
|
||||
}
|
||||
/**
|
||||
* Constructs a function value with the given captures. The mutability of the function
|
||||
* will be determined by the mutability of the capture values when evaluated.
|
||||
*/
|
||||
| {
|
||||
kind: 'CreateFunction';
|
||||
captures: Array<Place>;
|
||||
function: FunctionExpression | ObjectMethod;
|
||||
into: Place;
|
||||
}
|
||||
/**
|
||||
* Mutation of a value known to be immutable
|
||||
*/
|
||||
| {kind: 'MutateFrozen'; place: Place; error: CompilerErrorDetailOptions}
|
||||
/**
|
||||
* Mutation of a global
|
||||
*/
|
||||
| {
|
||||
kind: 'MutateGlobal';
|
||||
place: Place;
|
||||
error: CompilerErrorDetailOptions;
|
||||
}
|
||||
/**
|
||||
* Indicates a side-effect that is not safe during render
|
||||
*/
|
||||
| {kind: 'Impure'; place: Place; error: CompilerErrorDetailOptions}
|
||||
/**
|
||||
* Indicates that a given place is accessed during render. Used to distingush
|
||||
* hook arguments that are known to be called immediately vs those used for
|
||||
* event handlers/effects, and for JSX values known to be called during render
|
||||
* (tags, children) vs those that may be events/effect (other props).
|
||||
*/
|
||||
| {
|
||||
kind: 'Render';
|
||||
place: Place;
|
||||
};
|
||||
|
||||
export function hashEffect(effect: AliasingEffect): string {
|
||||
switch (effect.kind) {
|
||||
case 'Apply': {
|
||||
return [
|
||||
effect.kind,
|
||||
effect.receiver.identifier.id,
|
||||
effect.function.identifier.id,
|
||||
effect.mutatesFunction,
|
||||
effect.args
|
||||
.map(a => {
|
||||
if (a.kind === 'Hole') {
|
||||
return '';
|
||||
} else if (a.kind === 'Identifier') {
|
||||
return a.identifier.id;
|
||||
} else {
|
||||
return `...${a.place.identifier.id}`;
|
||||
}
|
||||
})
|
||||
.join(','),
|
||||
effect.into.identifier.id,
|
||||
].join(':');
|
||||
}
|
||||
case 'CreateFrom':
|
||||
case 'ImmutableCapture':
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'Capture': {
|
||||
return [
|
||||
effect.kind,
|
||||
effect.from.identifier.id,
|
||||
effect.into.identifier.id,
|
||||
].join(':');
|
||||
}
|
||||
case 'Create': {
|
||||
return [
|
||||
effect.kind,
|
||||
effect.into.identifier.id,
|
||||
effect.value,
|
||||
effect.reason,
|
||||
].join(':');
|
||||
}
|
||||
case 'Freeze': {
|
||||
return [effect.kind, effect.value.identifier.id, effect.reason].join(':');
|
||||
}
|
||||
case 'Impure':
|
||||
case 'Render':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
return [effect.kind, effect.place.identifier.id].join(':');
|
||||
}
|
||||
case 'Mutate':
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitive':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
return [effect.kind, effect.value.identifier.id].join(':');
|
||||
}
|
||||
case 'CreateFunction': {
|
||||
return [
|
||||
effect.kind,
|
||||
effect.into.identifier.id,
|
||||
// return places are a unique way to identify functions themselves
|
||||
effect.function.loweredFunc.func.returns.identifier.id,
|
||||
effect.captures.map(p => p.identifier.id).join(','),
|
||||
].join(':');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type AliasingSignature = {
|
||||
receiver: IdentifierId;
|
||||
params: Array<IdentifierId>;
|
||||
rest: IdentifierId | null;
|
||||
returns: IdentifierId;
|
||||
effects: Array<AliasingEffect>;
|
||||
temporaries: Array<Place>;
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
LoweredFunction,
|
||||
isRefOrRefValue,
|
||||
makeInstructionId,
|
||||
@@ -19,6 +20,10 @@ import {inferReactiveScopeVariables} from '../ReactiveScopes';
|
||||
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
|
||||
import {inferMutableRanges} from './InferMutableRanges';
|
||||
import inferReferenceEffects from './InferReferenceEffects';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
|
||||
import {inferMutationAliasingFunctionEffects} from './InferMutationAliasingFunctionEffects';
|
||||
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
|
||||
|
||||
export default function analyseFunctions(func: HIRFunction): void {
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
@@ -26,8 +31,12 @@ export default function analyseFunctions(func: HIRFunction): void {
|
||||
switch (instr.value.kind) {
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
lower(instr.value.loweredFunc.func);
|
||||
infer(instr.value.loweredFunc);
|
||||
if (!func.env.config.enableNewMutationAliasingModel) {
|
||||
lower(instr.value.loweredFunc.func);
|
||||
infer(instr.value.loweredFunc);
|
||||
} else {
|
||||
lowerWithMutationAliasing(instr.value.loweredFunc.func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset mutable range for outer inferReferenceEffects
|
||||
@@ -44,6 +53,87 @@ export default function analyseFunctions(func: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
|
||||
function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
/**
|
||||
* Phase 1: similar to lower(), but using the new mutation/aliasing inference
|
||||
*/
|
||||
analyseFunctions(fn);
|
||||
inferMutationAliasingEffects(fn, {isFunctionExpression: true});
|
||||
deadCodeElimination(fn);
|
||||
inferMutationAliasingRanges(fn, {isFunctionExpression: true});
|
||||
rewriteInstructionKindsBasedOnReassignment(fn);
|
||||
inferReactiveScopeVariables(fn);
|
||||
const effects = inferMutationAliasingFunctionEffects(fn);
|
||||
fn.env.logger?.debugLogIRs?.({
|
||||
kind: 'hir',
|
||||
name: 'AnalyseFunction (inner)',
|
||||
value: fn,
|
||||
});
|
||||
if (effects != null) {
|
||||
fn.aliasingEffects ??= [];
|
||||
fn.aliasingEffects?.push(...effects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2: populate the Effect of each context variable to use in inferring
|
||||
* the outer function. For example, InferMutationAliasingEffects uses context variable
|
||||
* effects to decide if the function may be mutable or not.
|
||||
*/
|
||||
const capturedOrMutated = new Set<IdentifierId>();
|
||||
for (const effect of effects ?? []) {
|
||||
switch (effect.kind) {
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'Capture':
|
||||
case 'CreateFrom': {
|
||||
capturedOrMutated.add(effect.from.identifier.id);
|
||||
break;
|
||||
}
|
||||
case 'Apply': {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
|
||||
loc: effect.function.loc,
|
||||
});
|
||||
}
|
||||
case 'Mutate':
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitive':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
capturedOrMutated.add(effect.value.identifier.id);
|
||||
break;
|
||||
}
|
||||
case 'Impure':
|
||||
case 'Render':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal':
|
||||
case 'CreateFunction':
|
||||
case 'Create':
|
||||
case 'Freeze':
|
||||
case 'ImmutableCapture': {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
effect,
|
||||
`Unexpected effect kind ${(effect as any).kind}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const operand of fn.context) {
|
||||
if (
|
||||
capturedOrMutated.has(operand.identifier.id) ||
|
||||
operand.effect === Effect.Capture
|
||||
) {
|
||||
operand.effect = Effect.Capture;
|
||||
} else {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lower(func: HIRFunction): void {
|
||||
analyseFunctions(func);
|
||||
inferReferenceEffects(func, {isFunctionExpression: true});
|
||||
|
||||
@@ -197,6 +197,7 @@ function makeManualMemoizationMarkers(
|
||||
deps: depsList,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
{
|
||||
@@ -208,6 +209,7 @@ function makeManualMemoizationMarkers(
|
||||
decl: {...memoDecl},
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -257,6 +257,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
value: deps,
|
||||
effects: null,
|
||||
},
|
||||
});
|
||||
value.args.push({...depsPlace, effect: Effect.Freeze});
|
||||
@@ -271,6 +272,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
value: deps,
|
||||
effects: null,
|
||||
},
|
||||
});
|
||||
value.args.push({...depsPlace, effect: Effect.Freeze});
|
||||
|
||||
@@ -324,7 +324,7 @@ function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
|
||||
return effect.kind === 'GlobalMutation';
|
||||
}
|
||||
|
||||
function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
export function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
if (abstractValue.reason.has(ValueReason.Global)) {
|
||||
return 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect';
|
||||
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
|
||||
@@ -339,6 +339,8 @@ function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
|
||||
return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.Effect)) {
|
||||
return 'Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect()';
|
||||
} else {
|
||||
return 'This mutates a variable that React considers immutable';
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export function inferMutableRanges(ir: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
|
||||
function areEqualMaps<T>(a: Map<T, T>, b: Map<T, T>): boolean {
|
||||
function areEqualMaps<T, U>(a: Map<T, U>, b: Map<T, U>): boolean {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 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 {HIRFunction, IdentifierId, Place, ValueKind, ValueReason} from '../HIR';
|
||||
import {getOrInsertDefault} from '../Utils/utils';
|
||||
import {AliasingEffect} from './AliasingEffects';
|
||||
|
||||
/**
|
||||
* This function tracks data flow within an inner function expression in order to
|
||||
* compute a set of data-flow aliasing effects describing data flow between the function's
|
||||
* params, context variables, and return value.
|
||||
*
|
||||
* For example, consider the following function expression:
|
||||
*
|
||||
* ```
|
||||
* (x) => { return [x, y] }
|
||||
* ```
|
||||
*
|
||||
* This function captures both param `x` and context variable `y` into the return value.
|
||||
* Unlike our previous inference which counted this as a mutation of x and y, we want to
|
||||
* build a signature for the function that describes the data flow. We would infer
|
||||
* `Capture x -> return, Capture y -> return` effects for this function.
|
||||
*
|
||||
* This function *also* propagates more ambient-style effects (MutateFrozen, MutateGlobal, Impure, Render)
|
||||
* from instructions within the function up to the function itself.
|
||||
*/
|
||||
export function inferMutationAliasingFunctionEffects(
|
||||
fn: HIRFunction,
|
||||
): Array<AliasingEffect> | null {
|
||||
const effects: Array<AliasingEffect> = [];
|
||||
|
||||
/**
|
||||
* Map used to identify tracked variables: params, context vars, return value
|
||||
* This is used to detect mutation/capturing/aliasing of params/context vars
|
||||
*/
|
||||
const tracked = new Map<IdentifierId, Place>();
|
||||
tracked.set(fn.returns.identifier.id, fn.returns);
|
||||
for (const operand of [...fn.context, ...fn.params]) {
|
||||
const place = operand.kind === 'Identifier' ? operand : operand.place;
|
||||
tracked.set(place.identifier.id, place);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track capturing/aliasing of context vars and params into each other and into the return.
|
||||
* We don't need to track locals and intermediate values, since we're only concerned with effects
|
||||
* as they relate to arguments visible outside the function.
|
||||
*
|
||||
* For each aliased identifier we track capture/alias/createfrom and then merge this with how
|
||||
* the value is used. Eg capturing an alias => capture. See joinEffects() helper.
|
||||
*/
|
||||
type AliasedIdentifier = {
|
||||
kind: AliasingKind;
|
||||
place: Place;
|
||||
};
|
||||
const dataFlow = new Map<IdentifierId, Array<AliasedIdentifier>>();
|
||||
|
||||
/*
|
||||
* Check for aliasing of tracked values. Also joins the effects of how the value is
|
||||
* used (@param kind) with the aliasing type of each value
|
||||
*/
|
||||
function lookup(
|
||||
place: Place,
|
||||
kind: AliasedIdentifier['kind'],
|
||||
): Array<AliasedIdentifier> | null {
|
||||
if (tracked.has(place.identifier.id)) {
|
||||
return [{kind, place}];
|
||||
}
|
||||
return (
|
||||
dataFlow.get(place.identifier.id)?.map(aliased => ({
|
||||
kind: joinEffects(aliased.kind, kind),
|
||||
place: aliased.place,
|
||||
})) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
// todo: fixpoint
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
const operands: Array<AliasedIdentifier> = [];
|
||||
for (const operand of phi.operands.values()) {
|
||||
const inputs = lookup(operand, 'Alias');
|
||||
if (inputs != null) {
|
||||
operands.push(...inputs);
|
||||
}
|
||||
}
|
||||
if (operands.length !== 0) {
|
||||
dataFlow.set(phi.place.identifier.id, operands);
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
if (instr.effects == null) continue;
|
||||
for (const effect of instr.effects) {
|
||||
if (
|
||||
effect.kind === 'Assign' ||
|
||||
effect.kind === 'Capture' ||
|
||||
effect.kind === 'Alias' ||
|
||||
effect.kind === 'CreateFrom'
|
||||
) {
|
||||
const from = lookup(effect.from, effect.kind);
|
||||
if (from == null) {
|
||||
continue;
|
||||
}
|
||||
const into = lookup(effect.into, 'Alias');
|
||||
if (into == null) {
|
||||
getOrInsertDefault(dataFlow, effect.into.identifier.id, []).push(
|
||||
...from,
|
||||
);
|
||||
} else {
|
||||
for (const aliased of into) {
|
||||
getOrInsertDefault(
|
||||
dataFlow,
|
||||
aliased.place.identifier.id,
|
||||
[],
|
||||
).push(...from);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
effect.kind === 'Create' ||
|
||||
effect.kind === 'CreateFunction'
|
||||
) {
|
||||
getOrInsertDefault(dataFlow, effect.into.identifier.id, [
|
||||
{kind: 'Alias', place: effect.into},
|
||||
]);
|
||||
} else if (
|
||||
effect.kind === 'MutateFrozen' ||
|
||||
effect.kind === 'MutateGlobal' ||
|
||||
effect.kind === 'Impure' ||
|
||||
effect.kind === 'Render'
|
||||
) {
|
||||
effects.push(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (block.terminal.kind === 'return') {
|
||||
const from = lookup(block.terminal.value, 'Alias');
|
||||
if (from != null) {
|
||||
getOrInsertDefault(dataFlow, fn.returns.identifier.id, []).push(
|
||||
...from,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create aliasing effects based on observed data flow
|
||||
let hasReturn = false;
|
||||
for (const [into, from] of dataFlow) {
|
||||
const input = tracked.get(into);
|
||||
if (input == null) {
|
||||
continue;
|
||||
}
|
||||
for (const aliased of from) {
|
||||
if (
|
||||
aliased.place.identifier.id === input.identifier.id ||
|
||||
!tracked.has(aliased.place.identifier.id)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const effect = {kind: aliased.kind, from: aliased.place, into: input};
|
||||
effects.push(effect);
|
||||
if (
|
||||
into === fn.returns.identifier.id &&
|
||||
(aliased.kind === 'Assign' || aliased.kind === 'CreateFrom')
|
||||
) {
|
||||
hasReturn = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: more precise return effect inference
|
||||
if (!hasReturn) {
|
||||
effects.unshift({
|
||||
kind: 'Create',
|
||||
into: fn.returns,
|
||||
value:
|
||||
fn.returnType.kind === 'Primitive'
|
||||
? ValueKind.Primitive
|
||||
: ValueKind.Mutable,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
});
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
|
||||
export enum MutationKind {
|
||||
None = 0,
|
||||
Conditional = 1,
|
||||
Definite = 2,
|
||||
}
|
||||
|
||||
type AliasingKind = 'Alias' | 'Capture' | 'CreateFrom' | 'Assign';
|
||||
function joinEffects(
|
||||
effect1: AliasingKind,
|
||||
effect2: AliasingKind,
|
||||
): AliasingKind {
|
||||
if (effect1 === 'Capture' || effect2 === 'Capture') {
|
||||
return 'Capture';
|
||||
} else if (effect1 === 'Assign' || effect2 === 'Assign') {
|
||||
return 'Assign';
|
||||
} else {
|
||||
return 'Alias';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,737 @@
|
||||
/**
|
||||
* 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 prettyFormat from 'pretty-format';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {
|
||||
BlockId,
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionId,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
|
||||
import {printFunction} from '../HIR';
|
||||
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
|
||||
import {MutationKind} from './InferMutationAliasingFunctionEffects';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
const DEBUG = false;
|
||||
const VERBOSE = false;
|
||||
|
||||
/**
|
||||
* Infers mutable ranges for all values in the program, using previously inferred
|
||||
* mutation/aliasing effects. This pass builds a data flow graph using the effects,
|
||||
* tracking an abstract notion of "when" each effect occurs relative to the others.
|
||||
* It then walks each mutation effect against the graph, updating the range of each
|
||||
* node that would be reachable at the "time" that the effect occurred.
|
||||
*
|
||||
* This pass also validates against invalid effects: any function that is reachable
|
||||
* by being called, or via a Render effect, is validated against mutating globals
|
||||
* or calling impure code.
|
||||
*
|
||||
* Note that this function also populates the outer function's aliasing effects with
|
||||
* any mutations that apply to its params or context variables. For example, a
|
||||
* function expression such as the following:
|
||||
*
|
||||
* ```
|
||||
* (x) => { x.y = true }
|
||||
* ```
|
||||
*
|
||||
* Would populate a `Mutate x` aliasing effect on the outer function.
|
||||
*/
|
||||
export function inferMutationAliasingRanges(
|
||||
fn: HIRFunction,
|
||||
{isFunctionExpression}: {isFunctionExpression: boolean},
|
||||
): Result<void, CompilerError> {
|
||||
if (VERBOSE) {
|
||||
console.log();
|
||||
console.log(printFunction(fn));
|
||||
}
|
||||
/**
|
||||
* Part 1: Infer mutable ranges for values. We build an abstract model of
|
||||
* values, the alias/capture edges between them, and the set of mutations.
|
||||
* Edges and mutations are ordered, with mutations processed against the
|
||||
* abstract model only after it is fully constructed by visiting all blocks
|
||||
* _and_ connecting phis. Phis are considered ordered at the time of the
|
||||
* phi node.
|
||||
*
|
||||
* This should (may?) mean that mutations are able to see the full state
|
||||
* of the graph and mark all the appropriate identifiers as mutated at
|
||||
* the correct point, accounting for both backward and forward edges.
|
||||
* Ie a mutation of x accounts for both values that flowed into x,
|
||||
* and values that x flowed into.
|
||||
*/
|
||||
const state = new AliasingState();
|
||||
type PendingPhiOperand = {from: Place; into: Place; index: number};
|
||||
const pendingPhis = new Map<BlockId, Array<PendingPhiOperand>>();
|
||||
const mutations: Array<{
|
||||
index: number;
|
||||
id: InstructionId;
|
||||
transitive: boolean;
|
||||
kind: MutationKind;
|
||||
place: Place;
|
||||
}> = [];
|
||||
const renders: Array<{index: number; place: Place}> = [];
|
||||
|
||||
let index = 0;
|
||||
|
||||
const errors = new CompilerError();
|
||||
|
||||
for (const param of [...fn.params, ...fn.context, fn.returns]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
state.create(place, {kind: 'Object'});
|
||||
}
|
||||
const seenBlocks = new Set<BlockId>();
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
state.create(phi.place, {kind: 'Phi'});
|
||||
for (const [pred, operand] of phi.operands) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
// NOTE: annotation required to actually typecheck and not silently infer `any`
|
||||
const blockPhis = getOrInsertWith<BlockId, Array<PendingPhiOperand>>(
|
||||
pendingPhis,
|
||||
pred,
|
||||
() => [],
|
||||
);
|
||||
blockPhis.push({from: operand, into: phi.place, index: index++});
|
||||
} else {
|
||||
state.assign(index++, operand, phi.place);
|
||||
}
|
||||
}
|
||||
}
|
||||
seenBlocks.add(block.id);
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
if (
|
||||
instr.value.kind === 'FunctionExpression' ||
|
||||
instr.value.kind === 'ObjectMethod'
|
||||
) {
|
||||
state.create(instr.lvalue, {
|
||||
kind: 'Function',
|
||||
function: instr.value.loweredFunc.func,
|
||||
});
|
||||
} else {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
state.create(lvalue, {kind: 'Object'});
|
||||
}
|
||||
}
|
||||
|
||||
if (instr.effects == null) continue;
|
||||
for (const effect of instr.effects) {
|
||||
if (effect.kind === 'Create') {
|
||||
state.create(effect.into, {kind: 'Object'});
|
||||
} else if (effect.kind === 'CreateFunction') {
|
||||
state.create(effect.into, {
|
||||
kind: 'Function',
|
||||
function: effect.function.loweredFunc.func,
|
||||
});
|
||||
} else if (effect.kind === 'CreateFrom') {
|
||||
state.createFrom(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Assign') {
|
||||
if (!state.nodes.has(effect.into.identifier)) {
|
||||
state.create(effect.into, {kind: 'Object'});
|
||||
}
|
||||
state.assign(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Alias') {
|
||||
state.assign(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Capture') {
|
||||
state.capture(index++, effect.from, effect.into);
|
||||
} else if (
|
||||
effect.kind === 'MutateTransitive' ||
|
||||
effect.kind === 'MutateTransitiveConditionally'
|
||||
) {
|
||||
mutations.push({
|
||||
index: index++,
|
||||
id: instr.id,
|
||||
transitive: true,
|
||||
kind:
|
||||
effect.kind === 'MutateTransitive'
|
||||
? MutationKind.Definite
|
||||
: MutationKind.Conditional,
|
||||
place: effect.value,
|
||||
});
|
||||
} else if (
|
||||
effect.kind === 'Mutate' ||
|
||||
effect.kind === 'MutateConditionally'
|
||||
) {
|
||||
mutations.push({
|
||||
index: index++,
|
||||
id: instr.id,
|
||||
transitive: false,
|
||||
kind:
|
||||
effect.kind === 'Mutate'
|
||||
? MutationKind.Definite
|
||||
: MutationKind.Conditional,
|
||||
place: effect.value,
|
||||
});
|
||||
} else if (
|
||||
effect.kind === 'MutateFrozen' ||
|
||||
effect.kind === 'MutateGlobal' ||
|
||||
effect.kind === 'Impure'
|
||||
) {
|
||||
errors.push(effect.error);
|
||||
} else if (effect.kind === 'Render') {
|
||||
renders.push({index: index++, place: effect.place});
|
||||
}
|
||||
}
|
||||
}
|
||||
const blockPhis = pendingPhis.get(block.id);
|
||||
if (blockPhis != null) {
|
||||
for (const {from, into, index} of blockPhis) {
|
||||
state.assign(index, from, into);
|
||||
}
|
||||
}
|
||||
if (block.terminal.kind === 'return') {
|
||||
state.assign(index++, block.terminal.value, fn.returns);
|
||||
}
|
||||
|
||||
if (
|
||||
(block.terminal.kind === 'maybe-throw' ||
|
||||
block.terminal.kind === 'return') &&
|
||||
block.terminal.effects != null
|
||||
) {
|
||||
for (const effect of block.terminal.effects) {
|
||||
if (effect.kind === 'Alias') {
|
||||
state.assign(index++, effect.from, effect.into);
|
||||
} else {
|
||||
CompilerError.invariant(effect.kind === 'Freeze', {
|
||||
reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
|
||||
loc: block.terminal.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (VERBOSE) {
|
||||
console.log(state.debug());
|
||||
console.log(pretty(mutations));
|
||||
}
|
||||
for (const mutation of mutations) {
|
||||
state.mutate(
|
||||
mutation.index,
|
||||
mutation.place.identifier,
|
||||
makeInstructionId(mutation.id + 1),
|
||||
mutation.transitive,
|
||||
mutation.kind,
|
||||
mutation.place.loc,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
for (const render of renders) {
|
||||
state.render(render.index, render.place.identifier, errors);
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(pretty([...state.nodes.keys()]));
|
||||
}
|
||||
fn.aliasingEffects ??= [];
|
||||
for (const param of [...fn.context, ...fn.params]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
const node = state.nodes.get(place.identifier);
|
||||
if (node == null) {
|
||||
continue;
|
||||
}
|
||||
let mutated = false;
|
||||
if (node.local != null) {
|
||||
if (node.local.kind === MutationKind.Conditional) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'MutateConditionally',
|
||||
value: {...place, loc: node.local.loc},
|
||||
});
|
||||
} else if (node.local.kind === MutationKind.Definite) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'Mutate',
|
||||
value: {...place, loc: node.local.loc},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (node.transitive != null) {
|
||||
if (node.transitive.kind === MutationKind.Conditional) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'MutateTransitiveConditionally',
|
||||
value: {...place, loc: node.transitive.loc},
|
||||
});
|
||||
} else if (node.transitive.kind === MutationKind.Definite) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'MutateTransitive',
|
||||
value: {...place, loc: node.transitive.loc},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (mutated) {
|
||||
place.effect = Effect.Capture;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 2
|
||||
* Add legacy operand-specific effects based on instruction effects and mutable ranges.
|
||||
* Also fixes up operand mutable ranges, making sure that start is non-zero if the value
|
||||
* is mutated (depended on by later passes like InferReactiveScopeVariables which uses this
|
||||
* to filter spurious mutations of globals, which we now guard against more precisely)
|
||||
*/
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
// TODO: we don't actually set these effects today!
|
||||
phi.place.effect = Effect.Store;
|
||||
const isPhiMutatedAfterCreation: boolean =
|
||||
phi.place.identifier.mutableRange.end >
|
||||
(block.instructions.at(0)?.id ?? block.terminal.id);
|
||||
for (const operand of phi.operands.values()) {
|
||||
operand.effect = isPhiMutatedAfterCreation
|
||||
? Effect.Capture
|
||||
: Effect.Read;
|
||||
}
|
||||
if (
|
||||
isPhiMutatedAfterCreation &&
|
||||
phi.place.identifier.mutableRange.start === 0
|
||||
) {
|
||||
/*
|
||||
* TODO: ideally we'd construct a precise start range, but what really
|
||||
* matters is that the phi's range appears mutable (end > start + 1)
|
||||
* so we just set the start to the previous instruction before this block
|
||||
*/
|
||||
const firstInstructionIdOfBlock =
|
||||
block.instructions.at(0)?.id ?? block.terminal.id;
|
||||
phi.place.identifier.mutableRange.start = makeInstructionId(
|
||||
firstInstructionIdOfBlock - 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
lvalue.effect = Effect.ConditionallyMutate;
|
||||
if (lvalue.identifier.mutableRange.start === 0) {
|
||||
lvalue.identifier.mutableRange.start = instr.id;
|
||||
}
|
||||
if (lvalue.identifier.mutableRange.end === 0) {
|
||||
lvalue.identifier.mutableRange.end = makeInstructionId(
|
||||
Math.max(instr.id + 1, lvalue.identifier.mutableRange.end),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
if (instr.effects == null) {
|
||||
continue;
|
||||
}
|
||||
const operandEffects = new Map<IdentifierId, Effect>();
|
||||
for (const effect of instr.effects) {
|
||||
switch (effect.kind) {
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'Capture':
|
||||
case 'CreateFrom': {
|
||||
const isMutatedOrReassigned =
|
||||
effect.into.identifier.mutableRange.end > instr.id;
|
||||
if (isMutatedOrReassigned) {
|
||||
operandEffects.set(effect.from.identifier.id, Effect.Capture);
|
||||
operandEffects.set(effect.into.identifier.id, Effect.Store);
|
||||
} else {
|
||||
operandEffects.set(effect.from.identifier.id, Effect.Read);
|
||||
operandEffects.set(effect.into.identifier.id, Effect.Store);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'CreateFunction':
|
||||
case 'Create': {
|
||||
break;
|
||||
}
|
||||
case 'Mutate': {
|
||||
operandEffects.set(effect.value.identifier.id, Effect.Store);
|
||||
break;
|
||||
}
|
||||
case 'Apply': {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
|
||||
loc: effect.function.loc,
|
||||
});
|
||||
}
|
||||
case 'MutateTransitive':
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
operandEffects.set(
|
||||
effect.value.identifier.id,
|
||||
Effect.ConditionallyMutate,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'Freeze': {
|
||||
operandEffects.set(effect.value.identifier.id, Effect.Freeze);
|
||||
break;
|
||||
}
|
||||
case 'ImmutableCapture': {
|
||||
// no-op, Read is the default
|
||||
break;
|
||||
}
|
||||
case 'Impure':
|
||||
case 'Render':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
effect,
|
||||
`Unexpected effect kind ${(effect as any).kind}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
const effect =
|
||||
operandEffects.get(lvalue.identifier.id) ??
|
||||
Effect.ConditionallyMutate;
|
||||
lvalue.effect = effect;
|
||||
}
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
if (
|
||||
operand.identifier.mutableRange.end > instr.id &&
|
||||
operand.identifier.mutableRange.start === 0
|
||||
) {
|
||||
operand.identifier.mutableRange.start = instr.id;
|
||||
}
|
||||
const effect = operandEffects.get(operand.identifier.id) ?? Effect.Read;
|
||||
operand.effect = effect;
|
||||
}
|
||||
|
||||
/**
|
||||
* This case is targeted at hoisted functions like:
|
||||
*
|
||||
* ```
|
||||
* x();
|
||||
* function x() { ... }
|
||||
* ```
|
||||
*
|
||||
* Which turns into:
|
||||
*
|
||||
* t0 = DeclareContext HoistedFunction x
|
||||
* t1 = LoadContext x
|
||||
* t2 = CallExpression t1 ( )
|
||||
* t3 = FunctionExpression ...
|
||||
* t4 = StoreContext Function x = t3
|
||||
*
|
||||
* If the function had captured mutable values, it would already have its
|
||||
* range extended to include the StoreContext. But if the function doesn't
|
||||
* capture any mutable values its range won't have been extended yet. We
|
||||
* want to ensure that the value is memoized along with the context variable,
|
||||
* not independently of it (bc of the way we do codegen for hoisted functions).
|
||||
* So here we check for StoreContext rvalues and if they haven't already had
|
||||
* their range extended to at least this instruction, we extend it.
|
||||
*/
|
||||
if (
|
||||
instr.value.kind === 'StoreContext' &&
|
||||
instr.value.value.identifier.mutableRange.end <= instr.id
|
||||
) {
|
||||
instr.value.value.identifier.mutableRange.end = makeInstructionId(
|
||||
instr.id + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (block.terminal.kind === 'return') {
|
||||
block.terminal.value.effect = isFunctionExpression
|
||||
? Effect.Read
|
||||
: Effect.Freeze;
|
||||
} else {
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (VERBOSE) {
|
||||
console.log(printFunction(fn));
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
|
||||
for (const effect of fn.aliasingEffects ?? []) {
|
||||
switch (effect.kind) {
|
||||
case 'Impure':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
errors.push(effect.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Node = {
|
||||
id: Identifier;
|
||||
createdFrom: Map<Identifier, number>;
|
||||
captures: Map<Identifier, number>;
|
||||
aliases: Map<Identifier, number>;
|
||||
edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>;
|
||||
transitive: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
local: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
value:
|
||||
| {kind: 'Object'}
|
||||
| {kind: 'Phi'}
|
||||
| {kind: 'Function'; function: HIRFunction};
|
||||
};
|
||||
class AliasingState {
|
||||
nodes: Map<Identifier, Node> = new Map();
|
||||
|
||||
create(place: Place, value: Node['value']): void {
|
||||
this.nodes.set(place.identifier, {
|
||||
id: place.identifier,
|
||||
createdFrom: new Map(),
|
||||
captures: new Map(),
|
||||
aliases: new Map(),
|
||||
edges: [],
|
||||
transitive: null,
|
||||
local: null,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
createFrom(index: number, from: Place, into: Place): void {
|
||||
this.create(into, {kind: 'Object'});
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
if (VERBOSE) {
|
||||
console.log(
|
||||
`skip: createFrom ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
|
||||
if (!toNode.createdFrom.has(from.identifier)) {
|
||||
toNode.createdFrom.set(from.identifier, index);
|
||||
}
|
||||
}
|
||||
|
||||
capture(index: number, from: Place, into: Place): void {
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
if (VERBOSE) {
|
||||
console.log(
|
||||
`skip: capture ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'capture'});
|
||||
if (!toNode.captures.has(from.identifier)) {
|
||||
toNode.captures.set(from.identifier, index);
|
||||
}
|
||||
}
|
||||
|
||||
assign(index: number, from: Place, into: Place): void {
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
if (VERBOSE) {
|
||||
console.log(
|
||||
`skip: assign ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
|
||||
if (!toNode.aliases.has(from.identifier)) {
|
||||
toNode.aliases.set(from.identifier, index);
|
||||
}
|
||||
}
|
||||
|
||||
render(index: number, start: Identifier, errors: CompilerError): void {
|
||||
const seen = new Set<Identifier>();
|
||||
const queue: Array<Identifier> = [start];
|
||||
while (queue.length !== 0) {
|
||||
const current = queue.pop()!;
|
||||
if (seen.has(current)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(current);
|
||||
const node = this.nodes.get(current);
|
||||
if (node == null || node.transitive != null || node.local != null) {
|
||||
continue;
|
||||
}
|
||||
if (node.value.kind === 'Function') {
|
||||
appendFunctionErrors(errors, node.value.function);
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push(alias);
|
||||
}
|
||||
for (const [alias, when] of node.aliases) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push(alias);
|
||||
}
|
||||
for (const [capture, when] of node.captures) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push(capture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutate(
|
||||
index: number,
|
||||
start: Identifier,
|
||||
end: InstructionId,
|
||||
transitive: boolean,
|
||||
kind: MutationKind,
|
||||
loc: SourceLocation,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
`mutate ix=${index} start=$${start.id} end=[${end}]${transitive ? ' transitive' : ''} kind=${kind}`,
|
||||
);
|
||||
}
|
||||
const seen = new Set<Identifier>();
|
||||
const queue: Array<{
|
||||
place: Identifier;
|
||||
transitive: boolean;
|
||||
direction: 'backwards' | 'forwards';
|
||||
}> = [{place: start, transitive, direction: 'backwards'}];
|
||||
while (queue.length !== 0) {
|
||||
const {place: current, transitive, direction} = queue.pop()!;
|
||||
if (seen.has(current)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(current);
|
||||
const node = this.nodes.get(current);
|
||||
if (node == null) {
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
`no node! ${printIdentifier(start)} for identifier ${printIdentifier(current)}`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
` mutate $${node.id.id} transitive=${transitive} direction=${direction}`,
|
||||
);
|
||||
}
|
||||
node.id.mutableRange.end = makeInstructionId(
|
||||
Math.max(node.id.mutableRange.end, end),
|
||||
);
|
||||
if (
|
||||
node.value.kind === 'Function' &&
|
||||
node.transitive == null &&
|
||||
node.local == null
|
||||
) {
|
||||
appendFunctionErrors(errors, node.value.function);
|
||||
}
|
||||
if (transitive) {
|
||||
if (node.transitive == null || node.transitive.kind < kind) {
|
||||
node.transitive = {kind, loc};
|
||||
}
|
||||
} else {
|
||||
if (node.local == null || node.local.kind < kind) {
|
||||
node.local = {kind, loc};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* all mutations affect "forward" edges by the rules:
|
||||
* - Capture a -> b, mutate(a) => mutate(b)
|
||||
* - Alias a -> b, mutate(a) => mutate(b)
|
||||
*/
|
||||
for (const edge of node.edges) {
|
||||
if (edge.index >= index) {
|
||||
break;
|
||||
}
|
||||
queue.push({place: edge.node, transitive, direction: 'forwards'});
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: alias, transitive: true, direction: 'backwards'});
|
||||
}
|
||||
if (direction === 'backwards' || node.value.kind !== 'Phi') {
|
||||
/**
|
||||
* all mutations affect backward alias edges by the rules:
|
||||
* - Alias a -> b, mutate(b) => mutate(a)
|
||||
* - Alias a -> b, mutateTransitive(b) => mutate(a)
|
||||
*
|
||||
* However, if we reached a phi because one of its inputs was mutated
|
||||
* (and we're advancing "forwards" through that node's edges), then
|
||||
* we know we've already processed the mutation at its source. The
|
||||
* phi's other inputs can't be affected.
|
||||
*/
|
||||
for (const [alias, when] of node.aliases) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: alias, transitive, direction: 'backwards'});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* but only transitive mutations affect captures
|
||||
*/
|
||||
if (transitive) {
|
||||
for (const [capture, when] of node.captures) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: capture, transitive, direction: 'backwards'});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (DEBUG) {
|
||||
const nodes = new Map();
|
||||
for (const id of seen) {
|
||||
const node = this.nodes.get(id);
|
||||
nodes.set(id.id, node);
|
||||
}
|
||||
console.log(pretty(nodes));
|
||||
}
|
||||
}
|
||||
|
||||
debug(): string {
|
||||
return pretty(this.nodes);
|
||||
}
|
||||
}
|
||||
|
||||
export function pretty(v: any): string {
|
||||
return prettyFormat(v, {
|
||||
plugins: [
|
||||
{
|
||||
test: v =>
|
||||
v !== null && typeof v === 'object' && v.kind === 'Identifier',
|
||||
serialize: v => printPlace(v),
|
||||
},
|
||||
{
|
||||
test: v =>
|
||||
v !== null &&
|
||||
typeof v === 'object' &&
|
||||
typeof v.declarationId === 'number',
|
||||
serialize: v =>
|
||||
`${printIdentifier(v)}:${v.mutableRange.start}:${v.mutableRange.end}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
eachTerminalOperand,
|
||||
eachTerminalSuccessor,
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {assertExhaustive, Set_isSuperset} from '../Utils/utils';
|
||||
import {
|
||||
inferTerminalFunctionEffects,
|
||||
inferInstructionFunctionEffects,
|
||||
@@ -779,7 +779,7 @@ function inferParam(
|
||||
* │ Mutable │───┘
|
||||
* └──────────────────────────┘
|
||||
*/
|
||||
function mergeValues(a: ValueKind, b: ValueKind): ValueKind {
|
||||
export function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
|
||||
if (a === b) {
|
||||
return a;
|
||||
} else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
|
||||
@@ -821,28 +821,16 @@ function mergeValues(a: ValueKind, b: ValueKind): ValueKind {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if `a` is a superset of `b`.
|
||||
*/
|
||||
function isSuperset<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): 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);
|
||||
const kind = mergeValueKinds(a.kind, b.kind);
|
||||
if (
|
||||
kind === a.kind &&
|
||||
kind === b.kind &&
|
||||
isSuperset(a.reason, b.reason) &&
|
||||
isSuperset(a.context, b.context)
|
||||
Set_isSuperset(a.reason, b.reason) &&
|
||||
Set_isSuperset(a.context, b.context)
|
||||
) {
|
||||
return a;
|
||||
}
|
||||
@@ -1989,7 +1977,7 @@ function areArgumentsImmutableAndNonMutating(
|
||||
return true;
|
||||
}
|
||||
|
||||
function getArgumentEffect(
|
||||
export function getArgumentEffect(
|
||||
signatureEffect: Effect | null,
|
||||
arg: Place | SpreadPattern,
|
||||
): Effect {
|
||||
|
||||
@@ -242,6 +242,7 @@ function rewriteBlock(
|
||||
type: null,
|
||||
loc: terminal.loc,
|
||||
},
|
||||
effects: null,
|
||||
});
|
||||
block.terminal = {
|
||||
kind: 'goto',
|
||||
@@ -270,5 +271,6 @@ function declareTemporary(
|
||||
type: null,
|
||||
loc: result.loc,
|
||||
},
|
||||
effects: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ export function inlineJsxTransform(
|
||||
type: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
currentBlockInstructions.push(varInstruction);
|
||||
@@ -167,6 +168,7 @@ export function inlineJsxTransform(
|
||||
},
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
currentBlockInstructions.push(devGlobalInstruction);
|
||||
@@ -220,6 +222,7 @@ export function inlineJsxTransform(
|
||||
type: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
thenBlockInstructions.push(reassignElseInstruction);
|
||||
@@ -292,6 +295,7 @@ export function inlineJsxTransform(
|
||||
],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
elseBlockInstructions.push(reactElementInstruction);
|
||||
@@ -309,6 +313,7 @@ export function inlineJsxTransform(
|
||||
type: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
elseBlockInstructions.push(reassignConditionalInstruction);
|
||||
@@ -436,6 +441,7 @@ function createSymbolProperty(
|
||||
binding: {kind: 'Global', name: 'Symbol'},
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolInstruction);
|
||||
@@ -450,6 +456,7 @@ function createSymbolProperty(
|
||||
property: makePropertyLiteral('for'),
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolForInstruction);
|
||||
@@ -463,6 +470,7 @@ function createSymbolProperty(
|
||||
value: symbolName,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolValueInstruction);
|
||||
@@ -478,6 +486,7 @@ function createSymbolProperty(
|
||||
args: [symbolValueInstruction.lvalue],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
const $$typeofProperty: ObjectProperty = {
|
||||
@@ -508,6 +517,7 @@ function createTagProperty(
|
||||
value: componentTag.name,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
tagProperty = {
|
||||
@@ -634,6 +644,7 @@ function createPropsProperties(
|
||||
elements: [...children],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(childrenPropInstruction);
|
||||
@@ -657,6 +668,7 @@ function createPropsProperties(
|
||||
value: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
refProperty = {
|
||||
@@ -678,6 +690,7 @@ function createPropsProperties(
|
||||
value: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
keyProperty = {
|
||||
@@ -711,6 +724,7 @@ function createPropsProperties(
|
||||
properties: props,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
propsProperty = {
|
||||
|
||||
@@ -146,6 +146,7 @@ function emitLoadLoweredContextCallee(
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: createTemporaryPlace(env, GeneratedSource),
|
||||
effects: null,
|
||||
value: loadGlobal,
|
||||
};
|
||||
}
|
||||
@@ -192,6 +193,7 @@ function emitPropertyLoad(
|
||||
lvalue: object,
|
||||
value: loadObj,
|
||||
id: makeInstructionId(0),
|
||||
effects: null,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
|
||||
@@ -206,6 +208,7 @@ function emitPropertyLoad(
|
||||
lvalue: element,
|
||||
value: loadProp,
|
||||
id: makeInstructionId(0),
|
||||
effects: null,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return {
|
||||
@@ -237,6 +240,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
kind: 'return',
|
||||
loc: GeneratedSource,
|
||||
value: arrayInstr.lvalue,
|
||||
effects: null,
|
||||
},
|
||||
preds: new Set(),
|
||||
phis: new Set(),
|
||||
@@ -250,6 +254,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
params: [obj],
|
||||
returnTypeAnnotation: null,
|
||||
returnType: makeType(),
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
body: {
|
||||
@@ -278,6 +283,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
lvalue: createTemporaryPlace(env, GeneratedSource),
|
||||
effects: null,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return fnInstr;
|
||||
@@ -294,6 +300,7 @@ function emitArrayInstr(elements: Array<Place>, env: Environment): Instruction {
|
||||
id: makeInstructionId(0),
|
||||
value: array,
|
||||
lvalue: arrayLvalue,
|
||||
effects: null,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return arrayInstr;
|
||||
|
||||
@@ -297,6 +297,7 @@ function emitOutlinedJsx(
|
||||
},
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
effects: null,
|
||||
};
|
||||
promoteTemporaryJsxTag(loadJsx.lvalue.identifier);
|
||||
const jsxExpr: Instruction = {
|
||||
@@ -312,6 +313,7 @@ function emitOutlinedJsx(
|
||||
openingLoc: GeneratedSource,
|
||||
closingLoc: GeneratedSource,
|
||||
},
|
||||
effects: null,
|
||||
};
|
||||
|
||||
return [loadJsx, jsxExpr];
|
||||
@@ -353,6 +355,7 @@ function emitOutlinedFn(
|
||||
kind: 'return',
|
||||
loc: GeneratedSource,
|
||||
value: instructions.at(-1)!.lvalue,
|
||||
effects: null,
|
||||
},
|
||||
preds: new Set(),
|
||||
phis: new Set(),
|
||||
@@ -366,6 +369,7 @@ function emitOutlinedFn(
|
||||
params: [propsObj],
|
||||
returnTypeAnnotation: null,
|
||||
returnType: makeType(),
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
body: {
|
||||
@@ -517,6 +521,7 @@ function emitDestructureProps(
|
||||
loc: GeneratedSource,
|
||||
value: propsObj,
|
||||
},
|
||||
effects: null,
|
||||
};
|
||||
return destructurePropsInstr;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
getHookKind,
|
||||
makeIdentifierName,
|
||||
} from '../HIR/HIR';
|
||||
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
|
||||
import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
|
||||
import {eachPatternOperand} from '../HIR/visitors';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {GuardKind} from '../Utils/RuntimeDiagnosticConstants';
|
||||
@@ -1310,7 +1310,7 @@ function codegenInstructionNullable(
|
||||
});
|
||||
CompilerError.invariant(value?.type === 'FunctionExpression', {
|
||||
reason: 'Expected a function as a function declaration value',
|
||||
description: null,
|
||||
description: `Got ${value == null ? String(value) : value.type} at ${printInstruction(instr)}`,
|
||||
loc: instr.value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -436,6 +436,7 @@ function makeLoadUseFireInstruction(
|
||||
value: instrValue,
|
||||
lvalue: {...useFirePlace},
|
||||
loc: GeneratedSource,
|
||||
effects: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -460,6 +461,7 @@ function makeLoadFireCalleeInstruction(
|
||||
},
|
||||
lvalue: {...loadedFireCallee},
|
||||
loc: GeneratedSource,
|
||||
effects: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -483,6 +485,7 @@ function makeCallUseFireInstruction(
|
||||
value: useFireCall,
|
||||
lvalue: {...useFireCallResultPlace},
|
||||
loc: GeneratedSource,
|
||||
effects: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -511,6 +514,7 @@ function makeStoreUseFireInstruction(
|
||||
},
|
||||
lvalue: fireFunctionBindingLValuePlace,
|
||||
loc: GeneratedSource,
|
||||
effects: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,21 @@ export function Set_intersect<T>(sets: Array<ReadonlySet<T>>): Set<T> {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if `a` is a superset of `b`.
|
||||
*/
|
||||
export function Set_isSuperset<T>(
|
||||
a: ReadonlySet<T>,
|
||||
b: ReadonlySet<T>,
|
||||
): boolean {
|
||||
for (const v of b) {
|
||||
if (!a.has(v)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function Iterable_some<T>(
|
||||
iter: Iterable<T>,
|
||||
pred: (item: T) => boolean,
|
||||
|
||||
@@ -58,8 +58,7 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
const effect = contextMutationEffects.get(operand.identifier.id);
|
||||
if (effect != null) {
|
||||
errors.push({
|
||||
reason: `This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update`,
|
||||
description: `Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables`,
|
||||
reason: `This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead`,
|
||||
loc: operand.loc,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
});
|
||||
@@ -112,6 +111,55 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
);
|
||||
if (knownMutation && knownMutation.kind === 'ContextMutation') {
|
||||
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
|
||||
} else if (
|
||||
fn.env.config.enableNewMutationAliasingModel &&
|
||||
value.loweredFunc.func.aliasingEffects != null
|
||||
) {
|
||||
const context = new Set(
|
||||
value.loweredFunc.func.context.map(p => p.identifier.id),
|
||||
);
|
||||
effects: for (const effect of value.loweredFunc.func
|
||||
.aliasingEffects) {
|
||||
switch (effect.kind) {
|
||||
case 'Mutate':
|
||||
case 'MutateTransitive': {
|
||||
const knownMutation = contextMutationEffects.get(
|
||||
effect.value.identifier.id,
|
||||
);
|
||||
if (knownMutation != null) {
|
||||
contextMutationEffects.set(
|
||||
lvalue.identifier.id,
|
||||
knownMutation,
|
||||
);
|
||||
} else if (
|
||||
context.has(effect.value.identifier.id) &&
|
||||
!isRefOrRefLikeMutableType(effect.value.identifier.type)
|
||||
) {
|
||||
contextMutationEffects.set(lvalue.identifier.id, {
|
||||
kind: 'ContextMutation',
|
||||
effect: Effect.Mutate,
|
||||
loc: effect.value.loc,
|
||||
places: new Set([effect.value]),
|
||||
});
|
||||
break effects;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
const knownMutation = contextMutationEffects.get(
|
||||
effect.value.identifier.id,
|
||||
);
|
||||
if (knownMutation != null) {
|
||||
contextMutationEffects.set(
|
||||
lvalue.identifier.id,
|
||||
knownMutation,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {makeArray, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
@@ -56,7 +57,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { makeArray, mutate } from "shared-runtime";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {makeArray, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
@@ -38,7 +39,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {identity, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
@@ -39,7 +40,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { identity, mutate } from "shared-runtime";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {identity, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
const Codes = {
|
||||
en: {name: 'English'},
|
||||
ja: {name: 'Japanese'},
|
||||
ko: {name: 'Korean'},
|
||||
zh: {name: 'Chinese'},
|
||||
};
|
||||
|
||||
function Component(a) {
|
||||
let keys;
|
||||
if (a) {
|
||||
keys = Object.keys(Codes);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
const options = keys.map(code => {
|
||||
const country = Codes[code];
|
||||
return {
|
||||
name: country.name,
|
||||
code,
|
||||
};
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
|
||||
<ValidateMemoization
|
||||
inputs={[]}
|
||||
output={options}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: false}],
|
||||
sequentialRenders: [
|
||||
{a: false},
|
||||
{a: true},
|
||||
{a: true},
|
||||
{a: false},
|
||||
{a: true},
|
||||
{a: false},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { ValidateMemoization } from "shared-runtime";
|
||||
|
||||
const Codes = {
|
||||
en: { name: "English" },
|
||||
ja: { name: "Japanese" },
|
||||
ko: { name: "Korean" },
|
||||
zh: { name: "Chinese" },
|
||||
};
|
||||
|
||||
function Component(a) {
|
||||
const $ = _c(4);
|
||||
let keys;
|
||||
if (a) {
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = Object.keys(Codes);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
keys = t0;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
let t0;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = keys.map(_temp);
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const options = t0;
|
||||
let t1;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = (
|
||||
<ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
|
||||
);
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = (
|
||||
<>
|
||||
{t1}
|
||||
<ValidateMemoization
|
||||
inputs={[]}
|
||||
output={options}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
function _temp(code) {
|
||||
const country = Codes[code];
|
||||
return { name: country.name, code };
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: false }],
|
||||
sequentialRenders: [
|
||||
{ a: false },
|
||||
{ a: true },
|
||||
{ a: true },
|
||||
{ a: false },
|
||||
{ a: true },
|
||||
{ a: false },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
const Codes = {
|
||||
en: {name: 'English'},
|
||||
ja: {name: 'Japanese'},
|
||||
ko: {name: 'Korean'},
|
||||
zh: {name: 'Chinese'},
|
||||
};
|
||||
|
||||
function Component(a) {
|
||||
let keys;
|
||||
if (a) {
|
||||
keys = Object.keys(Codes);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
const options = keys.map(code => {
|
||||
const country = Codes[code];
|
||||
return {
|
||||
name: country.name,
|
||||
code,
|
||||
};
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
|
||||
<ValidateMemoization
|
||||
inputs={[]}
|
||||
output={options}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: false}],
|
||||
sequentialRenders: [
|
||||
{a: false},
|
||||
{a: true},
|
||||
{a: true},
|
||||
{a: false},
|
||||
{a: true},
|
||||
{a: false},
|
||||
],
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Component() {
|
||||
const foo = () => {
|
||||
someGlobal = true;
|
||||
@@ -15,13 +16,13 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
1 | function Component() {
|
||||
2 | const foo = () => {
|
||||
> 3 | someGlobal = true;
|
||||
| ^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (3:3)
|
||||
4 | };
|
||||
5 | return <div {...foo} />;
|
||||
6 | }
|
||||
2 | function Component() {
|
||||
3 | const foo = () => {
|
||||
> 4 | someGlobal = true;
|
||||
| ^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (4:4)
|
||||
5 | };
|
||||
6 | return <div {...foo} />;
|
||||
7 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Component() {
|
||||
const foo = () => {
|
||||
someGlobal = true;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
|
||||
|
||||
import {useCallback, useEffect, useRef} from 'react';
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const params = useHook();
|
||||
const update = useCallback(
|
||||
partialParams => {
|
||||
const nextParams = {
|
||||
...params,
|
||||
...partialParams,
|
||||
};
|
||||
nextParams.param = 'value';
|
||||
console.log(nextParams);
|
||||
},
|
||||
[params]
|
||||
);
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
if (ref.current === null) {
|
||||
update();
|
||||
}
|
||||
}, [update]);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
18 | );
|
||||
19 | const ref = useRef(null);
|
||||
> 20 | useEffect(() => {
|
||||
| ^^^^^^^
|
||||
> 21 | if (ref.current === null) {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 22 | update();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 23 | }
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 24 | }, [update]);
|
||||
| ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (20:24)
|
||||
|
||||
InvalidReact: The function modifies a local variable here (14:14)
|
||||
25 |
|
||||
26 | return 'ok';
|
||||
27 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
|
||||
|
||||
import {useCallback, useEffect, useRef} from 'react';
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const params = useHook();
|
||||
const update = useCallback(
|
||||
partialParams => {
|
||||
const nextParams = {
|
||||
...params,
|
||||
...partialParams,
|
||||
};
|
||||
nextParams.param = 'value';
|
||||
console.log(nextParams);
|
||||
},
|
||||
[params]
|
||||
);
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
if (ref.current === null) {
|
||||
update();
|
||||
}
|
||||
}, [update]);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
import {useEffect, useState} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
@@ -33,45 +34,17 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
function Foo() {
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = [];
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
useEffect(() => setState(2), t0);
|
||||
|
||||
const [state, t1] = useState(0);
|
||||
const setState = t1;
|
||||
let t2;
|
||||
if ($[1] !== state) {
|
||||
t2 = <Stringify state={state} />;
|
||||
$[1] = state;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
sequentialRenders: [{}, {}],
|
||||
};
|
||||
## Error
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"state":2}</div>
|
||||
<div>{"state":2}</div>
|
||||
19 | useEffect(() => setState(2), []);
|
||||
20 |
|
||||
> 21 | const [state, setState] = useState(0);
|
||||
| ^^^^^^^^ InvalidReact: Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect(). Found mutation of `setState` (21:21)
|
||||
22 | return <Stringify state={state} />;
|
||||
23 | }
|
||||
24 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
import {useEffect, useState} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
@@ -24,7 +24,7 @@ function useFoo() {
|
||||
> 6 | cache.set('key', 'value');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 7 | });
|
||||
| ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (5:7)
|
||||
| ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (5:7)
|
||||
|
||||
InvalidReact: The function modifies a local variable here (6:6)
|
||||
8 | }
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component({prop1, prop2}) {
|
||||
'use memo';
|
||||
|
||||
const data = useIdentity(
|
||||
new Map([
|
||||
[0, 'value0'],
|
||||
[1, 'value1'],
|
||||
])
|
||||
);
|
||||
let i = 0;
|
||||
const items = [];
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i) + prop1}
|
||||
shouldInvokeFns={true}
|
||||
/>
|
||||
);
|
||||
i = i + 1;
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i) + prop2}
|
||||
shouldInvokeFns={true}
|
||||
/>
|
||||
);
|
||||
return <>{items}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prop1: 'prop1', prop2: 'prop2'}],
|
||||
sequentialRenders: [
|
||||
{prop1: 'prop1', prop2: 'prop2'},
|
||||
{prop1: 'prop1', prop2: 'prop2'},
|
||||
{prop1: 'changed', prop2: 'prop2'},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
20 | />
|
||||
21 | );
|
||||
> 22 | i = i + 1;
|
||||
| ^ InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX. Found mutation of `i` (22:22)
|
||||
23 | items.push(
|
||||
24 | <Stringify
|
||||
25 | key={i}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component({prop1, prop2}) {
|
||||
@@ -20,7 +20,7 @@ function Component() {
|
||||
5 | cache.set('key', 'value');
|
||||
6 | };
|
||||
> 7 | return <Foo fn={fn} />;
|
||||
| ^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:7)
|
||||
| ^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (7:7)
|
||||
|
||||
InvalidReact: The function modifies a local variable here (5:5)
|
||||
8 | }
|
||||
|
||||
@@ -26,7 +26,7 @@ function useFoo() {
|
||||
> 8 | cache.set('key', 'value');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 9 | };
|
||||
| ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:9)
|
||||
| ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (7:9)
|
||||
|
||||
InvalidReact: The function modifies a local variable here (8:8)
|
||||
10 | }
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableNewMutationAliasingModel
|
||||
/**
|
||||
* This hook returns a function that when called with an input object,
|
||||
* will return the result of mapping that input with the supplied map
|
||||
* function. Results are cached, so if the same input is passed again,
|
||||
* the same output object will be returned.
|
||||
*
|
||||
* Note that this technically violates the rules of React and is unsafe:
|
||||
* hooks must return immutable objects and be pure, and a function which
|
||||
* captures and mutates a value when called is inherently not pure.
|
||||
*
|
||||
* However, in this case it is technically safe _if_ the mapping function
|
||||
* is pure *and* the resulting objects are never modified. This is because
|
||||
* the function only caches: the result of `returnedFunction(someInput)`
|
||||
* strictly depends on `returnedFunction` and `someInput`, and cannot
|
||||
* otherwise change over time.
|
||||
*/
|
||||
hook useMemoMap<TInput: interface {}, TOutput>(
|
||||
map: TInput => TOutput
|
||||
): TInput => TOutput {
|
||||
return useMemo(() => {
|
||||
// The original issue is that `cache` was not memoized together with the returned
|
||||
// function. This was because neither appears to ever be mutated — the function
|
||||
// is known to mutate `cache` but the function isn't called.
|
||||
//
|
||||
// The fix is to detect cases like this — functions that are mutable but not called -
|
||||
// and ensure that their mutable captures are aliased together into the same scope.
|
||||
const cache = new WeakMap<TInput, TOutput>();
|
||||
return input => {
|
||||
let output = cache.get(input);
|
||||
if (output == null) {
|
||||
output = map(input);
|
||||
cache.set(input, output);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
}, [map]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
19 | map: TInput => TOutput
|
||||
20 | ): TInput => TOutput {
|
||||
> 21 | return useMemo(() => {
|
||||
| ^^^^^^^^^^^^^^^
|
||||
> 22 | // The original issue is that `cache` was not memoized together with the returned
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 23 | // function. This was because neither appears to ever be mutated — the function
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 24 | // is known to mutate `cache` but the function isn't called.
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 25 | //
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 26 | // The fix is to detect cases like this — functions that are mutable but not called -
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 27 | // and ensure that their mutable captures are aliased together into the same scope.
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 28 | const cache = new WeakMap<TInput, TOutput>();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 29 | return input => {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 30 | let output = cache.get(input);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 31 | if (output == null) {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 32 | output = map(input);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 33 | cache.set(input, output);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 34 | }
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 35 | return output;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 36 | };
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 37 | }, [map]);
|
||||
| ^^^^^^^^^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (21:37)
|
||||
|
||||
InvalidReact: The function modifies a local variable here (33:33)
|
||||
38 | }
|
||||
39 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @flow
|
||||
// @flow @enableNewMutationAliasingModel
|
||||
/**
|
||||
* This hook returns a function that when called with an input object,
|
||||
* will return the result of mapping that input with the supplied map
|
||||
@@ -18,7 +18,7 @@ function Component(props) {
|
||||
a.property = true;
|
||||
b.push(false);
|
||||
};
|
||||
return <div onClick={f()} />;
|
||||
return <div onClick={f} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -14,7 +14,7 @@ function Component(props) {
|
||||
a.property = true;
|
||||
b.push(false);
|
||||
};
|
||||
return <div onClick={f()} />;
|
||||
return <div onClick={f} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Foo() {
|
||||
const x = () => {
|
||||
window.href = 'foo';
|
||||
@@ -21,13 +22,13 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
1 | function Foo() {
|
||||
2 | const x = () => {
|
||||
> 3 | window.href = 'foo';
|
||||
| ^^^^^^ InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (3:3)
|
||||
4 | };
|
||||
5 | const y = {x};
|
||||
6 | return <Bar y={y} />;
|
||||
2 | function Foo() {
|
||||
3 | const x = () => {
|
||||
> 4 | window.href = 'foo';
|
||||
| ^^^^^^ InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (4:4)
|
||||
5 | };
|
||||
6 | const y = {x};
|
||||
7 | return <Bar y={y} />;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Foo() {
|
||||
const x = () => {
|
||||
window.href = 'foo';
|
||||
|
||||
@@ -22,7 +22,7 @@ function Component(props) {
|
||||
7 | return hasErrors;
|
||||
8 | }
|
||||
> 9 | return hasErrors();
|
||||
| ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$14 (9:9)
|
||||
| ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$15 (9:9)
|
||||
10 | }
|
||||
11 |
|
||||
```
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component({prop1, prop2}) {
|
||||
'use memo';
|
||||
|
||||
const data = useIdentity(
|
||||
new Map([
|
||||
[0, 'value0'],
|
||||
[1, 'value1'],
|
||||
])
|
||||
);
|
||||
let i = 0;
|
||||
const items = [];
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i) + prop1}
|
||||
shouldInvokeFns={true}
|
||||
/>
|
||||
);
|
||||
i = i + 1;
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i) + prop2}
|
||||
shouldInvokeFns={true}
|
||||
/>
|
||||
);
|
||||
return <>{items}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prop1: 'prop1', prop2: 'prop2'}],
|
||||
sequentialRenders: [
|
||||
{prop1: 'prop1', prop2: 'prop2'},
|
||||
{prop1: 'prop1', prop2: 'prop2'},
|
||||
{prop1: 'changed', prop2: 'prop2'},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify, useIdentity } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
"use memo";
|
||||
const $ = _c(12);
|
||||
const { prop1, prop2 } = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = new Map([
|
||||
[0, "value0"],
|
||||
[1, "value1"],
|
||||
]);
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const data = useIdentity(t1);
|
||||
let t2;
|
||||
if ($[1] !== data || $[2] !== prop1 || $[3] !== prop2) {
|
||||
let i = 0;
|
||||
const items = [];
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i) + prop1}
|
||||
shouldInvokeFns={true}
|
||||
/>,
|
||||
);
|
||||
i = i + 1;
|
||||
|
||||
const t3 = i;
|
||||
let t4;
|
||||
if ($[5] !== data || $[6] !== i || $[7] !== prop2) {
|
||||
t4 = () => data.get(i) + prop2;
|
||||
$[5] = data;
|
||||
$[6] = i;
|
||||
$[7] = prop2;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
let t5;
|
||||
if ($[9] !== t3 || $[10] !== t4) {
|
||||
t5 = <Stringify key={t3} onClick={t4} shouldInvokeFns={true} />;
|
||||
$[9] = t3;
|
||||
$[10] = t4;
|
||||
$[11] = t5;
|
||||
} else {
|
||||
t5 = $[11];
|
||||
}
|
||||
items.push(t5);
|
||||
t2 = <>{items}</>;
|
||||
$[1] = data;
|
||||
$[2] = prop1;
|
||||
$[3] = prop2;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ prop1: "prop1", prop2: "prop2" }],
|
||||
sequentialRenders: [
|
||||
{ prop1: "prop1", prop2: "prop2" },
|
||||
{ prop1: "prop1", prop2: "prop2" },
|
||||
{ prop1: "changed", prop2: "prop2" },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"onClick":{"kind":"Function","result":"value1prop1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
|
||||
<div>{"onClick":{"kind":"Function","result":"value1prop1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
|
||||
<div>{"onClick":{"kind":"Function","result":"value1changed"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
|
||||
@@ -0,0 +1,93 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component({value}) {
|
||||
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
|
||||
useIdentity(null);
|
||||
const derived = arr.filter(Boolean);
|
||||
return (
|
||||
<Stringify>
|
||||
{derived.at(0)}
|
||||
{derived.at(-1)}
|
||||
</Stringify>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
function Component(t0) {
|
||||
const $ = _c(13);
|
||||
const { value } = t0;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = { value: "foo" };
|
||||
t2 = { value: "bar" };
|
||||
$[0] = t1;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
t2 = $[1];
|
||||
}
|
||||
let t3;
|
||||
if ($[2] !== value) {
|
||||
t3 = [t1, t2, { value }];
|
||||
$[2] = value;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const arr = t3;
|
||||
useIdentity(null);
|
||||
let t4;
|
||||
if ($[4] !== arr) {
|
||||
t4 = arr.filter(Boolean);
|
||||
$[4] = arr;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
const derived = t4;
|
||||
let t5;
|
||||
if ($[6] !== derived) {
|
||||
t5 = derived.at(0);
|
||||
$[6] = derived;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
let t6;
|
||||
if ($[8] !== derived) {
|
||||
t6 = derived.at(-1);
|
||||
$[8] = derived;
|
||||
$[9] = t6;
|
||||
} else {
|
||||
t6 = $[9];
|
||||
}
|
||||
let t7;
|
||||
if ($[10] !== t5 || $[11] !== t6) {
|
||||
t7 = (
|
||||
<Stringify>
|
||||
{t5}
|
||||
{t6}
|
||||
</Stringify>
|
||||
);
|
||||
$[10] = t5;
|
||||
$[11] = t6;
|
||||
$[12] = t7;
|
||||
} else {
|
||||
t7 = $[12];
|
||||
}
|
||||
return t7;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,12 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component({value}) {
|
||||
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
|
||||
useIdentity(null);
|
||||
const derived = arr.filter(Boolean);
|
||||
return (
|
||||
<Stringify>
|
||||
{derived.at(0)}
|
||||
{derived.at(-1)}
|
||||
</Stringify>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component(props) {
|
||||
// This item is part of the receiver, should be memoized
|
||||
const item = {a: props.a};
|
||||
const items = [item];
|
||||
const mapped = items.map(item => item);
|
||||
// mapped[0].a = null;
|
||||
return mapped;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: {id: 42}}],
|
||||
isComponent: false,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
function Component(props) {
|
||||
const $ = _c(6);
|
||||
let t0;
|
||||
if ($[0] !== props.a) {
|
||||
t0 = { a: props.a };
|
||||
$[0] = props.a;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const item = t0;
|
||||
let t1;
|
||||
if ($[2] !== item) {
|
||||
t1 = [item];
|
||||
$[2] = item;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const items = t1;
|
||||
let t2;
|
||||
if ($[4] !== items) {
|
||||
t2 = items.map(_temp);
|
||||
$[4] = items;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const mapped = t2;
|
||||
return mapped;
|
||||
}
|
||||
function _temp(item_0) {
|
||||
return item_0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: { id: 42 } }],
|
||||
isComponent: false,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) [{"a":{"id":42}}]
|
||||
@@ -0,0 +1,15 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component(props) {
|
||||
// This item is part of the receiver, should be memoized
|
||||
const item = {a: props.a};
|
||||
const items = [item];
|
||||
const mapped = items.map(item => item);
|
||||
// mapped[0].a = null;
|
||||
return mapped;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: {id: 42}}],
|
||||
isComponent: false,
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component({a, b, c}) {
|
||||
const x = [];
|
||||
x.push(a);
|
||||
const merged = {b}; // could be mutated by mutate(x) below
|
||||
x.push(merged);
|
||||
mutate(x);
|
||||
const independent = {c}; // can't be later mutated
|
||||
x.push(independent);
|
||||
return <Foo value={x} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { a, b, c } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a || $[1] !== b || $[2] !== c) {
|
||||
const x = [];
|
||||
x.push(a);
|
||||
const merged = { b };
|
||||
x.push(merged);
|
||||
mutate(x);
|
||||
let t2;
|
||||
if ($[4] !== c) {
|
||||
t2 = { c };
|
||||
$[4] = c;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const independent = t2;
|
||||
x.push(independent);
|
||||
t1 = <Foo value={x} />;
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = c;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,11 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component({a, b, c}) {
|
||||
const x = [];
|
||||
x.push(a);
|
||||
const merged = {b}; // could be mutated by mutate(x) below
|
||||
x.push(merged);
|
||||
mutate(x);
|
||||
const independent = {c}; // can't be later mutated
|
||||
x.push(independent);
|
||||
return <Foo value={x} />;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component({a, b}) {
|
||||
const x = {a};
|
||||
const y = [b];
|
||||
const f = () => {
|
||||
y.x = x;
|
||||
mutate(y);
|
||||
};
|
||||
f();
|
||||
return <div>{x}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
function Component(t0) {
|
||||
const $ = _c(3);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const x = { a };
|
||||
const y = [b];
|
||||
const f = () => {
|
||||
y.x = x;
|
||||
mutate(y);
|
||||
};
|
||||
|
||||
f();
|
||||
t1 = <div>{x}</div>;
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,11 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component({a, b}) {
|
||||
const x = {a};
|
||||
const y = [b];
|
||||
const f = () => {
|
||||
y.x = x;
|
||||
mutate(y);
|
||||
};
|
||||
f();
|
||||
return <div>{x}</div>;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component({a, b}) {
|
||||
const x = {a};
|
||||
const y = [b];
|
||||
y.x = x;
|
||||
mutate(y);
|
||||
return <div>{x}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
function Component(t0) {
|
||||
const $ = _c(3);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const x = { a };
|
||||
const y = [b];
|
||||
y.x = x;
|
||||
mutate(y);
|
||||
t1 = <div>{x}</div>;
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,8 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component({a, b}) {
|
||||
const x = {a};
|
||||
const y = [b];
|
||||
y.x = x;
|
||||
mutate(y);
|
||||
return <div>{x}</div>;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
import {arrayPush, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({prop1, prop2}) {
|
||||
'use memo';
|
||||
|
||||
let x = [{value: prop1}];
|
||||
let z;
|
||||
while (x.length < 2) {
|
||||
// there's a phi here for x (value before the loop and the reassignment later)
|
||||
|
||||
// this mutation occurs before the reassigned value
|
||||
arrayPush(x, {value: prop2});
|
||||
|
||||
if (x[0].value === prop1) {
|
||||
x = [{value: prop2}];
|
||||
const y = x;
|
||||
z = y[0];
|
||||
}
|
||||
}
|
||||
z.other = true;
|
||||
return <Stringify z={z} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prop1: 0, prop2: 'a'}],
|
||||
sequentialRenders: [
|
||||
{prop1: 0, prop2: 'a'},
|
||||
{prop1: 1, prop2: 'a'},
|
||||
{prop1: 1, prop2: 'b'},
|
||||
{prop1: 0, prop2: 'b'},
|
||||
{prop1: 0, prop2: 'a'},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
import { arrayPush, Stringify } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
"use memo";
|
||||
const $ = _c(5);
|
||||
const { prop1, prop2 } = t0;
|
||||
let z;
|
||||
if ($[0] !== prop1 || $[1] !== prop2) {
|
||||
let x = [{ value: prop1 }];
|
||||
while (x.length < 2) {
|
||||
arrayPush(x, { value: prop2 });
|
||||
if (x[0].value === prop1) {
|
||||
x = [{ value: prop2 }];
|
||||
const y = x;
|
||||
z = y[0];
|
||||
}
|
||||
}
|
||||
|
||||
z.other = true;
|
||||
$[0] = prop1;
|
||||
$[1] = prop2;
|
||||
$[2] = z;
|
||||
} else {
|
||||
z = $[2];
|
||||
}
|
||||
let t1;
|
||||
if ($[3] !== z) {
|
||||
t1 = <Stringify z={z} />;
|
||||
$[3] = z;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ prop1: 0, prop2: "a" }],
|
||||
sequentialRenders: [
|
||||
{ prop1: 0, prop2: "a" },
|
||||
{ prop1: 1, prop2: "a" },
|
||||
{ prop1: 1, prop2: "b" },
|
||||
{ prop1: 0, prop2: "b" },
|
||||
{ prop1: 0, prop2: "a" },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"z":{"value":"a","other":true}}</div>
|
||||
<div>{"z":{"value":"a","other":true}}</div>
|
||||
<div>{"z":{"value":"b","other":true}}</div>
|
||||
<div>{"z":{"value":"b","other":true}}</div>
|
||||
<div>{"z":{"value":"a","other":true}}</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
import {arrayPush, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({prop1, prop2}) {
|
||||
'use memo';
|
||||
|
||||
let x = [{value: prop1}];
|
||||
let z;
|
||||
while (x.length < 2) {
|
||||
// there's a phi here for x (value before the loop and the reassignment later)
|
||||
|
||||
// this mutation occurs before the reassigned value
|
||||
arrayPush(x, {value: prop2});
|
||||
|
||||
if (x[0].value === prop1) {
|
||||
x = [{value: prop2}];
|
||||
const y = x;
|
||||
z = y[0];
|
||||
}
|
||||
}
|
||||
z.other = true;
|
||||
return <Stringify z={z} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prop1: 0, prop2: 'a'}],
|
||||
sequentialRenders: [
|
||||
{prop1: 0, prop2: 'a'},
|
||||
{prop1: 1, prop2: 'a'},
|
||||
{prop1: 1, prop2: 'b'},
|
||||
{prop1: 0, prop2: 'b'},
|
||||
{prop1: 0, prop2: 'a'},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component() {
|
||||
let local;
|
||||
|
||||
const reassignLocal = newValue => {
|
||||
local = newValue;
|
||||
};
|
||||
|
||||
const onClick = newValue => {
|
||||
reassignLocal('hello');
|
||||
|
||||
if (local === newValue) {
|
||||
// Without React Compiler, `reassignLocal` is freshly created
|
||||
// on each render, capturing a binding to the latest `local`,
|
||||
// such that invoking reassignLocal will reassign the same
|
||||
// binding that we are observing in the if condition, and
|
||||
// we reach this branch
|
||||
console.log('`local` was updated!');
|
||||
} else {
|
||||
// With React Compiler enabled, `reassignLocal` is only created
|
||||
// once, capturing a binding to `local` in that render pass.
|
||||
// Therefore, calling `reassignLocal` will reassign the wrong
|
||||
// version of `local`, and not update the binding we are checking
|
||||
// in the if condition.
|
||||
//
|
||||
// To protect against this, we disallow reassigning locals from
|
||||
// functions that escape
|
||||
throw new Error('`local` not updated!');
|
||||
}
|
||||
};
|
||||
|
||||
return <button onClick={onClick}>Submit</button>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
3 |
|
||||
4 | const reassignLocal = newValue => {
|
||||
> 5 | local = newValue;
|
||||
| ^^^^^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `local` cannot be reassigned after render (5:5)
|
||||
6 | };
|
||||
7 |
|
||||
8 | const onClick = newValue => {
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
function Component() {
|
||||
let local;
|
||||
|
||||
const reassignLocal = newValue => {
|
||||
local = newValue;
|
||||
};
|
||||
|
||||
const onClick = newValue => {
|
||||
reassignLocal('hello');
|
||||
|
||||
if (local === newValue) {
|
||||
// Without React Compiler, `reassignLocal` is freshly created
|
||||
// on each render, capturing a binding to the latest `local`,
|
||||
// such that invoking reassignLocal will reassign the same
|
||||
// binding that we are observing in the if condition, and
|
||||
// we reach this branch
|
||||
console.log('`local` was updated!');
|
||||
} else {
|
||||
// With React Compiler enabled, `reassignLocal` is only created
|
||||
// once, capturing a binding to `local` in that render pass.
|
||||
// Therefore, calling `reassignLocal` will reassign the wrong
|
||||
// version of `local`, and not update the binding we are checking
|
||||
// in the if condition.
|
||||
//
|
||||
// To protect against this, we disallow reassigning locals from
|
||||
// functions that escape
|
||||
throw new Error('`local` not updated!');
|
||||
}
|
||||
};
|
||||
|
||||
return <button onClick={onClick}>Submit</button>;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel
|
||||
import {useCallback} from 'react';
|
||||
import {makeArray} from 'shared-runtime';
|
||||
|
||||
// This case is already unsound in source, so we can safely bailout
|
||||
function Foo(props) {
|
||||
let x = [];
|
||||
x.push(props);
|
||||
|
||||
// makeArray() is captured, but depsList contains [props]
|
||||
const cb = useCallback(() => [x], [x]);
|
||||
|
||||
x = makeArray();
|
||||
|
||||
return cb;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
9 |
|
||||
10 | // makeArray() is captured, but depsList contains [props]
|
||||
> 11 | const cb = useCallback(() => [x], [x]);
|
||||
| ^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly (11:11)
|
||||
|
||||
CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. (11:11)
|
||||
12 |
|
||||
13 | x = makeArray();
|
||||
14 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel
|
||||
import {useCallback} from 'react';
|
||||
import {makeArray} from 'shared-runtime';
|
||||
|
||||
// This case is already unsound in source, so we can safely bailout
|
||||
function Foo(props) {
|
||||
let x = [];
|
||||
x.push(props);
|
||||
|
||||
// makeArray() is captured, but depsList contains [props]
|
||||
const cb = useCallback(() => [x], [x]);
|
||||
|
||||
x = makeArray();
|
||||
|
||||
return cb;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component({a, b}) {
|
||||
const x = {a};
|
||||
useFreeze(x);
|
||||
x.y = true;
|
||||
return <div>error</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
3 | const x = {a};
|
||||
4 | useFreeze(x);
|
||||
> 5 | x.y = true;
|
||||
| ^ InvalidReact: This mutates a variable that React considers immutable (5:5)
|
||||
6 | return <div>error</div>;
|
||||
7 | }
|
||||
8 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component({a, b}) {
|
||||
const x = {a};
|
||||
useFreeze(x);
|
||||
x.y = true;
|
||||
return <div>error</div>;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
const items = (() => {
|
||||
if (props.cond) {
|
||||
return [];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
items?.push(props.a);
|
||||
return items;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: {}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = _c(3);
|
||||
let items;
|
||||
if ($[0] !== props.a || $[1] !== props.cond) {
|
||||
let t0;
|
||||
if (props.cond) {
|
||||
t0 = [];
|
||||
} else {
|
||||
t0 = null;
|
||||
}
|
||||
items = t0;
|
||||
|
||||
items?.push(props.a);
|
||||
$[0] = props.a;
|
||||
$[1] = props.cond;
|
||||
$[2] = items;
|
||||
} else {
|
||||
items = $[2];
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: {} }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) null
|
||||
@@ -0,0 +1,16 @@
|
||||
function Component(props) {
|
||||
const items = (() => {
|
||||
if (props.cond) {
|
||||
return [];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
items?.push(props.a);
|
||||
return items;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: {}}],
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const x = {a, b};
|
||||
const f = () => {
|
||||
const y = [x];
|
||||
return y[0];
|
||||
};
|
||||
const x0 = f();
|
||||
const z = [x0];
|
||||
const x1 = z[0];
|
||||
x1.key = 'value';
|
||||
return <Stringify x={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 1}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(3);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const x = { a, b };
|
||||
const f = () => {
|
||||
const y = [x];
|
||||
return y[0];
|
||||
};
|
||||
|
||||
const x0 = f();
|
||||
const z = [x0];
|
||||
const x1 = z[0];
|
||||
x1.key = "value";
|
||||
t1 = <Stringify x={x} />;
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 0, b: 1 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"x":{"a":0,"b":1,"key":"value"}}</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const x = {a, b};
|
||||
const f = () => {
|
||||
const y = [x];
|
||||
return y[0];
|
||||
};
|
||||
const x0 = f();
|
||||
const z = [x0];
|
||||
const x1 = z[0];
|
||||
x1.key = 'value';
|
||||
return <Stringify x={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 1}],
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const x = {a, b};
|
||||
const y = [x];
|
||||
const f = () => {
|
||||
const x0 = y[0];
|
||||
return [x0];
|
||||
};
|
||||
const z = f();
|
||||
const x1 = z[0];
|
||||
x1.key = 'value';
|
||||
return <Stringify x={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 1}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(3);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const x = { a, b };
|
||||
const y = [x];
|
||||
const f = () => {
|
||||
const x0 = y[0];
|
||||
return [x0];
|
||||
};
|
||||
|
||||
const z = f();
|
||||
const x1 = z[0];
|
||||
x1.key = "value";
|
||||
t1 = <Stringify x={x} />;
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 0, b: 1 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"x":{"a":0,"b":1,"key":"value"}}</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const x = {a, b};
|
||||
const y = [x];
|
||||
const f = () => {
|
||||
const x0 = y[0];
|
||||
return [x0];
|
||||
};
|
||||
const z = f();
|
||||
const x1 = z[0];
|
||||
x1.key = 'value';
|
||||
return <Stringify x={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 1}],
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const x = {a, b};
|
||||
const y = [x];
|
||||
const x0 = y[0];
|
||||
const z = [x0];
|
||||
const x1 = z[0];
|
||||
x1.key = 'value';
|
||||
return <Stringify x={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 1}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(3);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const x = { a, b };
|
||||
const y = [x];
|
||||
const x0 = y[0];
|
||||
const z = [x0];
|
||||
const x1 = z[0];
|
||||
x1.key = "value";
|
||||
t1 = <Stringify x={x} />;
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 0, b: 1 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"x":{"a":0,"b":1,"key":"value"}}</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const x = {a, b};
|
||||
const y = [x];
|
||||
const x0 = y[0];
|
||||
const z = [x0];
|
||||
const x1 = z[0];
|
||||
x1.key = 'value';
|
||||
return <Stringify x={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 1}],
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component({a, b}) {
|
||||
const x = {};
|
||||
const y = {x};
|
||||
const z = y.x;
|
||||
z.true = false;
|
||||
return <div>{z}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
function Component(t0) {
|
||||
const $ = _c(1);
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const x = {};
|
||||
const y = { x };
|
||||
const z = y.x;
|
||||
z.true = false;
|
||||
t1 = <div>{z}</div>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,8 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component({a, b}) {
|
||||
const x = {};
|
||||
const y = {x};
|
||||
const z = y.x;
|
||||
z.true = false;
|
||||
return <div>{z}</div>;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
import {useState} from 'react';
|
||||
import {useIdentity} from 'shared-runtime';
|
||||
|
||||
function useMakeCallback({obj}: {obj: {value: number}}) {
|
||||
const [state, setState] = useState(0);
|
||||
const cb = () => {
|
||||
if (obj.value !== state) setState(obj.value);
|
||||
};
|
||||
useIdentity();
|
||||
cb();
|
||||
return [cb];
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useMakeCallback,
|
||||
params: [{obj: {value: 1}}],
|
||||
sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
import { useState } from "react";
|
||||
import { useIdentity } from "shared-runtime";
|
||||
|
||||
function useMakeCallback(t0) {
|
||||
const $ = _c(5);
|
||||
const { obj } = t0;
|
||||
const [state, setState] = useState(0);
|
||||
let t1;
|
||||
if ($[0] !== obj.value || $[1] !== state) {
|
||||
t1 = () => {
|
||||
if (obj.value !== state) {
|
||||
setState(obj.value);
|
||||
}
|
||||
};
|
||||
$[0] = obj.value;
|
||||
$[1] = state;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const cb = t1;
|
||||
|
||||
useIdentity();
|
||||
cb();
|
||||
let t2;
|
||||
if ($[3] !== cb) {
|
||||
t2 = [cb];
|
||||
$[3] = cb;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useMakeCallback,
|
||||
params: [{ obj: { value: 1 } }],
|
||||
sequentialRenders: [{ obj: { value: 1 } }, { obj: { value: 2 } }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) ["[[ function params=0 ]]"]
|
||||
["[[ function params=0 ]]"]
|
||||
@@ -0,0 +1,18 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
import {useState} from 'react';
|
||||
import {useIdentity} from 'shared-runtime';
|
||||
|
||||
function useMakeCallback({obj}: {obj: {value: number}}) {
|
||||
const [state, setState] = useState(0);
|
||||
const cb = () => {
|
||||
if (obj.value !== state) setState(obj.value);
|
||||
};
|
||||
useIdentity();
|
||||
cb();
|
||||
return [cb];
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useMakeCallback,
|
||||
params: [{obj: {value: 1}}],
|
||||
sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}],
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component({a, b, c}) {
|
||||
const x = [a, b];
|
||||
const f = () => {
|
||||
maybeMutate(x);
|
||||
// different dependency to force this not to merge with x's scope
|
||||
console.log(c);
|
||||
};
|
||||
return <Foo onClick={f} value={x} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
function Component(t0) {
|
||||
const $ = _c(9);
|
||||
const { a, b, c } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
t1 = [a, b];
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const x = t1;
|
||||
let t2;
|
||||
if ($[3] !== c || $[4] !== x) {
|
||||
t2 = () => {
|
||||
maybeMutate(x);
|
||||
|
||||
console.log(c);
|
||||
};
|
||||
$[3] = c;
|
||||
$[4] = x;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const f = t2;
|
||||
let t3;
|
||||
if ($[6] !== f || $[7] !== x) {
|
||||
t3 = <Foo onClick={f} value={x} />;
|
||||
$[6] = f;
|
||||
$[7] = x;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,10 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component({a, b, c}) {
|
||||
const x = [a, b];
|
||||
const f = () => {
|
||||
maybeMutate(x);
|
||||
// different dependency to force this not to merge with x's scope
|
||||
console.log(c);
|
||||
};
|
||||
return <Foo onClick={f} value={x} />;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
function ReactiveRefInEffect(props) {
|
||||
const ref1 = useRef('initial value');
|
||||
const ref2 = useRef('initial value');
|
||||
let ref;
|
||||
if (props.foo) {
|
||||
ref = ref1;
|
||||
} else {
|
||||
ref = ref2;
|
||||
}
|
||||
useEffect(() => print(ref));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
function ReactiveRefInEffect(props) {
|
||||
const $ = _c(4);
|
||||
const ref1 = useRef("initial value");
|
||||
const ref2 = useRef("initial value");
|
||||
let ref;
|
||||
if ($[0] !== props.foo) {
|
||||
if (props.foo) {
|
||||
ref = ref1;
|
||||
} else {
|
||||
ref = ref2;
|
||||
}
|
||||
$[0] = props.foo;
|
||||
$[1] = ref;
|
||||
} else {
|
||||
ref = $[1];
|
||||
}
|
||||
let t0;
|
||||
if ($[2] !== ref) {
|
||||
t0 = () => print(ref);
|
||||
$[2] = ref;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t0 = $[3];
|
||||
}
|
||||
useEffect(t0);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,12 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
function ReactiveRefInEffect(props) {
|
||||
const ref1 = useRef('initial value');
|
||||
const ref2 = useRef('initial value');
|
||||
let ref;
|
||||
if (props.foo) {
|
||||
ref = ref1;
|
||||
} else {
|
||||
ref = ref2;
|
||||
}
|
||||
useEffect(() => print(ref));
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
function useHook({el1, el2}) {
|
||||
const s = new Set();
|
||||
const arr = makeArray(el1);
|
||||
s.add(arr);
|
||||
// Mutate after store
|
||||
arr.push(el2);
|
||||
|
||||
s.add(makeArray(el2));
|
||||
return s.size;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
function useHook(t0) {
|
||||
const $ = _c(5);
|
||||
const { el1, el2 } = t0;
|
||||
let s;
|
||||
if ($[0] !== el1 || $[1] !== el2) {
|
||||
s = new Set();
|
||||
const arr = makeArray(el1);
|
||||
s.add(arr);
|
||||
|
||||
arr.push(el2);
|
||||
let t1;
|
||||
if ($[3] !== el2) {
|
||||
t1 = makeArray(el2);
|
||||
$[3] = el2;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
s.add(t1);
|
||||
$[0] = el1;
|
||||
$[1] = el2;
|
||||
$[2] = s;
|
||||
} else {
|
||||
s = $[2];
|
||||
}
|
||||
return s.size;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,11 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
function useHook({el1, el2}) {
|
||||
const s = new Set();
|
||||
const arr = makeArray(el1);
|
||||
s.add(arr);
|
||||
// Mutate after store
|
||||
arr.push(el2);
|
||||
|
||||
s.add(makeArray(el2));
|
||||
return s.size;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enablePropagateDepsInHIR @enableNewMutationAliasingModel
|
||||
function useFoo(props) {
|
||||
let x = [];
|
||||
x.push(props.bar);
|
||||
// todo: the below should memoize separately from the above
|
||||
// my guess is that the phi causes the different `x` identifiers
|
||||
// to get added to an alias group. this is where we need to track
|
||||
// the actual state of the alias groups at the time of the mutation
|
||||
props.cond ? (({x} = {x: {}}), ([x] = [[]]), x.push(props.foo)) : null;
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{cond: false, foo: 2, bar: 55}],
|
||||
sequentialRenders: [
|
||||
{cond: false, foo: 2, bar: 55},
|
||||
{cond: false, foo: 3, bar: 55},
|
||||
{cond: true, foo: 3, bar: 55},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR @enableNewMutationAliasingModel
|
||||
function useFoo(props) {
|
||||
const $ = _c(5);
|
||||
let x;
|
||||
if ($[0] !== props.bar) {
|
||||
x = [];
|
||||
x.push(props.bar);
|
||||
$[0] = props.bar;
|
||||
$[1] = x;
|
||||
} else {
|
||||
x = $[1];
|
||||
}
|
||||
if ($[2] !== props.cond || $[3] !== props.foo) {
|
||||
props.cond ? (([x] = [[]]), x.push(props.foo)) : null;
|
||||
$[2] = props.cond;
|
||||
$[3] = props.foo;
|
||||
$[4] = x;
|
||||
} else {
|
||||
x = $[4];
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{ cond: false, foo: 2, bar: 55 }],
|
||||
sequentialRenders: [
|
||||
{ cond: false, foo: 2, bar: 55 },
|
||||
{ cond: false, foo: 3, bar: 55 },
|
||||
{ cond: true, foo: 3, bar: 55 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) [55]
|
||||
[55]
|
||||
[3]
|
||||
@@ -0,0 +1,21 @@
|
||||
// @enablePropagateDepsInHIR @enableNewMutationAliasingModel
|
||||
function useFoo(props) {
|
||||
let x = [];
|
||||
x.push(props.bar);
|
||||
// todo: the below should memoize separately from the above
|
||||
// my guess is that the phi causes the different `x` identifiers
|
||||
// to get added to an alias group. this is where we need to track
|
||||
// the actual state of the alias groups at the time of the mutation
|
||||
props.cond ? (({x} = {x: {}}), ([x] = [[]]), x.push(props.foo)) : null;
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{cond: false, foo: 2, bar: 55}],
|
||||
sequentialRenders: [
|
||||
{cond: false, foo: 2, bar: 55},
|
||||
{cond: false, foo: 3, bar: 55},
|
||||
{cond: true, foo: 3, bar: 55},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component({a, b}) {
|
||||
const x = [a];
|
||||
const y = {b};
|
||||
mutate(y);
|
||||
y.x = x;
|
||||
return <div>{y}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a) {
|
||||
t1 = [a];
|
||||
$[0] = a;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const x = t1;
|
||||
let t2;
|
||||
if ($[2] !== b || $[3] !== x) {
|
||||
const y = { b };
|
||||
mutate(y);
|
||||
y.x = x;
|
||||
t2 = <div>{y}</div>;
|
||||
$[2] = b;
|
||||
$[3] = x;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user