mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[compiler] Further improve new range inference
Further refine the abstract interpretation for InferMutationAliasingRanges to account for forward data flow: Alias/Capture a -> b, mutate(a) => mutate(b) ie mutation affects aliases of the place being mutated, not just things that place aliased.
Fixes inference of which context vars of a function mutate, using the precise inference from the Range pass.
Adds MutateFrozen and MutateGlobal effects but they're not fully hooked up yet.
ghstack-source-id: 0d83b1afa9
Pull Request resolved: https://github.com/facebook/react/pull/33411
This commit is contained in:
@@ -115,6 +115,14 @@ export class CompilerErrorDetail {
|
||||
export class CompilerError extends Error {
|
||||
details: Array<CompilerErrorDetail> = [];
|
||||
|
||||
static from(details: Array<CompilerErrorDetailOptions>): CompilerError {
|
||||
const error = new CompilerError();
|
||||
for (const detail of details) {
|
||||
error.push(detail);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
static invariant(
|
||||
condition: unknown,
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
|
||||
@@ -1003,6 +1003,12 @@ export function printAliasingEffect(effect: AliasingEffect): string {
|
||||
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)}`;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(effect, `Unexpected kind '${(effect as any).kind}'`);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Place,
|
||||
isRefOrRefValue,
|
||||
makeInstructionId,
|
||||
printFunction,
|
||||
} from '../HIR';
|
||||
import {deadCodeElimination} from '../Optimization';
|
||||
import {inferReactiveScopeVariables} from '../ReactiveScopes';
|
||||
@@ -72,7 +73,10 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
value: fn,
|
||||
});
|
||||
const effects = inferMutationAliasingFunctionEffects(fn);
|
||||
fn.aliasingEffects = effects;
|
||||
if (effects != null) {
|
||||
fn.aliasingEffects ??= [];
|
||||
fn.aliasingEffects?.push(...effects);
|
||||
}
|
||||
|
||||
const capturedOrMutated = new Set<IdentifierId>();
|
||||
for (const effect of effects ?? []) {
|
||||
@@ -97,6 +101,8 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
capturedOrMutated.add(effect.value.identifier.id);
|
||||
break;
|
||||
}
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal':
|
||||
case 'CreateFunction':
|
||||
case 'Create':
|
||||
case 'Freeze':
|
||||
@@ -114,7 +120,10 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
}
|
||||
|
||||
for (const operand of fn.context) {
|
||||
if (capturedOrMutated.has(operand.identifier.id)) {
|
||||
if (
|
||||
capturedOrMutated.has(operand.identifier.id) ||
|
||||
operand.effect === Effect.Capture
|
||||
) {
|
||||
operand.effect = Effect.Capture;
|
||||
} else {
|
||||
operand.effect = Effect.Read;
|
||||
|
||||
+111
-39
@@ -5,7 +5,13 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, Effect, ValueKind} from '..';
|
||||
import {
|
||||
CompilerError,
|
||||
CompilerErrorDetailOptions,
|
||||
Effect,
|
||||
ErrorSeverity,
|
||||
ValueKind,
|
||||
} from '..';
|
||||
import {
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
@@ -214,11 +220,6 @@ function inferBlock(
|
||||
instructionSignature = computeSignatureForInstruction(state.env, instr);
|
||||
instructionSignatureCache.set(instr, instructionSignature);
|
||||
}
|
||||
/*
|
||||
* console.log(
|
||||
* printInstruction({...instr, effects: [...instructionSignature.effects]}),
|
||||
* );
|
||||
*/
|
||||
const effects = applySignature(
|
||||
state,
|
||||
instructionSignature,
|
||||
@@ -244,6 +245,7 @@ function applySignature(
|
||||
instruction: Instruction,
|
||||
effectInstructionValueCache: Map<AliasingEffect, InstructionValue>,
|
||||
): Array<AliasingEffect> | null {
|
||||
const effects: Array<AliasingEffect> = [];
|
||||
/**
|
||||
* For function instructions, eagerly validate that they aren't mutating
|
||||
* a known-frozen value.
|
||||
@@ -260,27 +262,33 @@ function applySignature(
|
||||
for (const effect of aliasingEffects) {
|
||||
if (effect.kind === 'Mutate' || effect.kind === 'MutateTransitive') {
|
||||
const value = state.kind(effect.value);
|
||||
if (value.kind === ValueKind.Frozen) {
|
||||
if (DEBUG) {
|
||||
console.log(printInstruction(instruction));
|
||||
console.log(printAliasingEffect(effect));
|
||||
console.log(prettyFormat(state.debugAbstractValue(value)));
|
||||
switch (value.kind) {
|
||||
case ValueKind.Global:
|
||||
case ValueKind.Frozen: {
|
||||
const reason = getWriteErrorReason({
|
||||
kind: value.kind,
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
effects.push({
|
||||
kind:
|
||||
value.kind === ValueKind.Frozen
|
||||
? 'MutateFrozen'
|
||||
: 'MutateGlobal',
|
||||
place: effect.value,
|
||||
error: {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason,
|
||||
description:
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `Found mutation of \`${effect.value.identifier.name}\``
|
||||
: null,
|
||||
loc: effect.value.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
const reason = getWriteErrorReason({
|
||||
kind: value.kind,
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
CompilerError.throwInvalidReact({
|
||||
reason,
|
||||
description:
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `Found mutation of \`${effect.value.identifier.name}\``
|
||||
: null,
|
||||
loc: effect.value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,7 +304,6 @@ function applySignature(
|
||||
console.log(printInstruction(instruction));
|
||||
}
|
||||
|
||||
const effects: Array<AliasingEffect> = [];
|
||||
for (const effect of signature.effects) {
|
||||
applyEffect(
|
||||
state,
|
||||
@@ -429,6 +436,19 @@ function applyEffect(
|
||||
}
|
||||
}
|
||||
});
|
||||
for (const operand of effect.function.loweredFunc.func.context) {
|
||||
if (operand.effect !== Effect.Capture) {
|
||||
continue;
|
||||
}
|
||||
const kind = state.kind(operand).kind;
|
||||
if (
|
||||
kind === ValueKind.Primitive ||
|
||||
kind == ValueKind.Frozen ||
|
||||
kind == ValueKind.Global
|
||||
) {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
state.initialize(effect.function, {
|
||||
kind: isMutable ? ValueKind.Mutable : ValueKind.Frozen,
|
||||
reason: new Set([]),
|
||||
@@ -742,24 +762,36 @@ function applyEffect(
|
||||
console.log(printAliasingEffect(effect));
|
||||
console.log(prettyFormat(state.debugAbstractValue(value)));
|
||||
}
|
||||
|
||||
const reason = getWriteErrorReason({
|
||||
kind: value.kind,
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
CompilerError.throwInvalidReact({
|
||||
reason,
|
||||
description:
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `Found mutation of \`${effect.value.identifier.name}\``
|
||||
: null,
|
||||
loc: effect.value.loc,
|
||||
suggestions: null,
|
||||
effects.push({
|
||||
kind:
|
||||
value.kind === ValueKind.Frozen ? 'MutateFrozen' : 'MutateGlobal',
|
||||
place: effect.value,
|
||||
error: {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason,
|
||||
description:
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `Found mutation of \`${effect.value.identifier.name}\``
|
||||
: null,
|
||||
loc: effect.value.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
effects.push(effect);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
effect,
|
||||
@@ -933,7 +965,11 @@ class InferenceState {
|
||||
kind: ValueKind.Frozen,
|
||||
reason: new Set([reason]),
|
||||
});
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
if (
|
||||
value.kind === 'FunctionExpression' &&
|
||||
(this.env.config.enablePreserveExistingMemoizationGuarantees ||
|
||||
this.env.config.enableTransitivelyFreezeFunctionExpressions)
|
||||
) {
|
||||
for (const place of value.loweredFunc.func.context) {
|
||||
this.freeze(place, reason);
|
||||
}
|
||||
@@ -1552,15 +1588,31 @@ function computeSignatureForInstruction(
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'StartMemoize':
|
||||
case 'FinishMemoize': {
|
||||
if (env.config.enablePreserveExistingMemoizationGuarantees) {
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
effects.push({
|
||||
kind: 'Freeze',
|
||||
value: operand,
|
||||
reason: ValueReason.Other,
|
||||
});
|
||||
}
|
||||
}
|
||||
effects.push({
|
||||
kind: 'Create',
|
||||
into: lvalue,
|
||||
value: ValueKind.Primitive,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'TaggedTemplateExpression':
|
||||
case 'BinaryExpression':
|
||||
case 'Debugger':
|
||||
case 'FinishMemoize':
|
||||
case 'JSXText':
|
||||
case 'MetaProperty':
|
||||
case 'Primitive':
|
||||
case 'RegExpLiteral':
|
||||
case 'StartMemoize':
|
||||
case 'TemplateLiteral':
|
||||
case 'UnaryExpression':
|
||||
case 'UnsupportedNode': {
|
||||
@@ -1879,6 +1931,14 @@ function computeEffectsForSignature(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
const values = substitutions.get(effect.place.identifier.id) ?? [];
|
||||
for (const value of values) {
|
||||
effects.push({kind: effect.kind, place: value, error: effect.error});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Mutate':
|
||||
case 'MutateTransitive':
|
||||
case 'MutateTransitiveConditionally':
|
||||
@@ -2165,6 +2225,18 @@ export type AliasingEffect =
|
||||
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;
|
||||
};
|
||||
|
||||
export type AliasingSignatureEffect = AliasingEffect;
|
||||
|
||||
+9
-90
@@ -5,14 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isPrimitiveType,
|
||||
Place,
|
||||
ScopeId,
|
||||
ValueKind,
|
||||
} from '../HIR';
|
||||
import {HIRFunction, IdentifierId, Place, ScopeId, ValueKind} from '../HIR';
|
||||
import {getOrInsertDefault} from '../Utils/utils';
|
||||
import {AliasingEffect} from './InferMutationAliasingEffects';
|
||||
|
||||
@@ -28,35 +21,6 @@ export function inferMutationAliasingFunctionEffects(
|
||||
const tracked = new Map<IdentifierId, Place>();
|
||||
tracked.set(fn.returns.identifier.id, fn.returns);
|
||||
|
||||
/**
|
||||
* For each reactive scope we track whether there are known/conditional local and transitive
|
||||
* mutations. This is used to recover precise mutation effects for each of the params and
|
||||
* context variables.
|
||||
*/
|
||||
const trackedScopes = new Map<
|
||||
ScopeId,
|
||||
{local: MutationKind; transitive: MutationKind}
|
||||
>();
|
||||
for (const operand of fn.context) {
|
||||
tracked.set(operand.identifier.id, operand);
|
||||
if (operand.identifier.scope != null) {
|
||||
getOrInsertDefault(trackedScopes, operand.identifier.scope.id, {
|
||||
local: MutationKind.None,
|
||||
transitive: MutationKind.None,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const param of fn.params) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
tracked.set(place.identifier.id, place);
|
||||
if (place.identifier.scope != null) {
|
||||
getOrInsertDefault(trackedScopes, place.identifier.scope.id, {
|
||||
local: MutationKind.None,
|
||||
transitive: MutationKind.None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -132,36 +96,10 @@ export function inferMutationAliasingFunctionEffects(
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
effect.kind === 'Mutate' &&
|
||||
effect.value.identifier.scope != null &&
|
||||
trackedScopes.has(effect.value.identifier.scope.id)
|
||||
effect.kind === 'MutateFrozen' ||
|
||||
effect.kind === 'MutateGlobal'
|
||||
) {
|
||||
const scope = trackedScopes.get(effect.value.identifier.scope.id)!;
|
||||
scope.local = MutationKind.Definite;
|
||||
} else if (
|
||||
effect.kind === 'MutateConditionally' &&
|
||||
effect.value.identifier.scope != null &&
|
||||
trackedScopes.has(effect.value.identifier.scope.id)
|
||||
) {
|
||||
const scope = trackedScopes.get(effect.value.identifier.scope.id)!;
|
||||
scope.local = Math.max(MutationKind.Conditional, scope.local);
|
||||
} else if (
|
||||
effect.kind === 'MutateTransitive' &&
|
||||
effect.value.identifier.scope != null &&
|
||||
trackedScopes.has(effect.value.identifier.scope.id)
|
||||
) {
|
||||
const scope = trackedScopes.get(effect.value.identifier.scope.id)!;
|
||||
scope.transitive = MutationKind.Definite;
|
||||
} else if (
|
||||
effect.kind === 'MutateTransitiveConditionally' &&
|
||||
effect.value.identifier.scope != null &&
|
||||
trackedScopes.has(effect.value.identifier.scope.id)
|
||||
) {
|
||||
const scope = trackedScopes.get(effect.value.identifier.scope.id)!;
|
||||
scope.transitive = Math.max(
|
||||
MutationKind.Conditional,
|
||||
scope.transitive,
|
||||
);
|
||||
effects.push(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,26 +113,6 @@ export function inferMutationAliasingFunctionEffects(
|
||||
}
|
||||
}
|
||||
|
||||
// Create mutation effects based on observed mutation types
|
||||
for (const value of tracked.values()) {
|
||||
if (
|
||||
value.identifier.id === fn.returns.identifier.id ||
|
||||
value.identifier.scope == null
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const scope = trackedScopes.get(value.identifier.scope.id)!;
|
||||
if (scope.local === MutationKind.Definite) {
|
||||
effects.push({kind: 'Mutate', value});
|
||||
} else if (scope.local === MutationKind.Conditional) {
|
||||
effects.push({kind: 'MutateConditionally', value});
|
||||
}
|
||||
if (scope.transitive === MutationKind.Definite) {
|
||||
effects.push({kind: 'MutateTransitive', value});
|
||||
} else if (scope.transitive === MutationKind.Conditional) {
|
||||
effects.push({kind: 'MutateTransitiveConditionally', value});
|
||||
}
|
||||
}
|
||||
// Create aliasing effects based on observed data flow
|
||||
let hasReturn = false;
|
||||
for (const [into, from] of dataFlow) {
|
||||
@@ -218,16 +136,17 @@ export function inferMutationAliasingFunctionEffects(
|
||||
effects.push({
|
||||
kind: 'Create',
|
||||
into: fn.returns,
|
||||
value: isPrimitiveType(fn.returns.identifier)
|
||||
? ValueKind.Primitive
|
||||
: ValueKind.Mutable,
|
||||
value:
|
||||
fn.returnType.kind === 'Primitive'
|
||||
? ValueKind.Primitive
|
||||
: ValueKind.Mutable,
|
||||
});
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
|
||||
enum MutationKind {
|
||||
export enum MutationKind {
|
||||
None = 0,
|
||||
Conditional = 1,
|
||||
Definite = 2,
|
||||
|
||||
+293
-153
@@ -6,8 +6,9 @@
|
||||
*/
|
||||
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {CompilerError} from '..';
|
||||
import {
|
||||
BlockId,
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
@@ -23,7 +24,8 @@ import {
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
|
||||
import {printFunction} from '../HIR';
|
||||
import {printInstruction} from '../HIR/PrintHIR';
|
||||
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
|
||||
import {MutationKind} from './InferMutationAliasingFunctionEffects';
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
@@ -33,80 +35,181 @@ const DEBUG = false;
|
||||
export function inferMutationAliasingRanges(fn: HIRFunction): void {
|
||||
if (DEBUG) {
|
||||
console.log();
|
||||
console.log();
|
||||
console.log();
|
||||
console.log();
|
||||
console.log();
|
||||
console.log();
|
||||
console.log();
|
||||
console.log();
|
||||
console.log();
|
||||
console.log();
|
||||
console.log();
|
||||
console.log(printFunction(fn));
|
||||
}
|
||||
/**
|
||||
* Part 1: Infer mutable ranges for values. We build an abstract model
|
||||
* of the effect types, distinguishing values which can capture references
|
||||
* to other values and variables, which can point to one or more values.
|
||||
* 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.
|
||||
*
|
||||
* Transitive mutation marks value identifiers as mutated and also walks
|
||||
* into the identifiers captured by that abstract value: local mutation
|
||||
* only marks the top-level identifiers as mutated.
|
||||
*
|
||||
* TODO: add a fixpoint.
|
||||
* 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();
|
||||
for (const param of fn.context) {
|
||||
state.create(param);
|
||||
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;
|
||||
}> = [];
|
||||
|
||||
let index = 0;
|
||||
|
||||
for (const param of [...fn.params, ...fn.context, fn.returns]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
state.create(place);
|
||||
}
|
||||
const seenBlocks = new Set<BlockId>();
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
state.create(phi.place);
|
||||
for (const operand of phi.operands.values()) {
|
||||
state.alias(operand, phi.place);
|
||||
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) {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
state.create(lvalue);
|
||||
if (lvalue.identifier.mutableRange.start === 0) {
|
||||
lvalue.identifier.mutableRange.start = instr.id;
|
||||
}
|
||||
lvalue.identifier.mutableRange.end = makeInstructionId(
|
||||
Math.max(instr.id + 1, lvalue.identifier.mutableRange.end),
|
||||
);
|
||||
}
|
||||
|
||||
if (instr.effects == null) continue;
|
||||
for (const effect of instr.effects) {
|
||||
if (effect.kind === 'Create' || effect.kind === 'CreateFunction') {
|
||||
state.create(effect.into);
|
||||
} else if (effect.kind === 'Assign') {
|
||||
state.assign(effect.from, effect.into);
|
||||
} else if (effect.kind === 'Alias' || effect.kind === 'CreateFrom') {
|
||||
state.alias(effect.from, effect.into);
|
||||
} else if (
|
||||
effect.kind === 'Assign' ||
|
||||
effect.kind === 'Alias' ||
|
||||
effect.kind === 'CreateFrom'
|
||||
) {
|
||||
state.assign(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Capture') {
|
||||
state.capture(effect.from, effect.into);
|
||||
state.capture(index++, effect.from, effect.into);
|
||||
} else if (
|
||||
effect.kind === 'MutateTransitive' ||
|
||||
effect.kind === 'MutateTransitiveConditionally'
|
||||
) {
|
||||
state.mutateTransitive(
|
||||
effect.value.identifier,
|
||||
makeInstructionId(instr.id + 1),
|
||||
effect.value.loc,
|
||||
);
|
||||
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'
|
||||
) {
|
||||
state.mutate(
|
||||
effect.value.identifier,
|
||||
makeInstructionId(instr.id + 1),
|
||||
effect.value.loc,
|
||||
);
|
||||
mutations.push({
|
||||
index: index++,
|
||||
id: instr.id,
|
||||
transitive: false,
|
||||
kind:
|
||||
effect.kind === 'Mutate'
|
||||
? MutationKind.Definite
|
||||
: MutationKind.Conditional,
|
||||
place: effect.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(printInstruction(instr));
|
||||
console.log(state.debug());
|
||||
}
|
||||
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 (DEBUG) {
|
||||
console.log(state.debug());
|
||||
console.log(pretty(mutations));
|
||||
}
|
||||
for (const mutation of mutations) {
|
||||
state.mutate(
|
||||
mutation.index,
|
||||
mutation.place,
|
||||
makeInstructionId(mutation.id + 1),
|
||||
mutation.transitive,
|
||||
mutation.kind,
|
||||
);
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(state.debug());
|
||||
}
|
||||
fn.aliasingEffects ??= [];
|
||||
for (const param of fn.context) {
|
||||
const node = state.nodes.get(param.identifier);
|
||||
if (node == null) {
|
||||
continue;
|
||||
}
|
||||
let mutated = false;
|
||||
if (node.local === MutationKind.Conditional) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'MutateConditionally',
|
||||
value: param,
|
||||
});
|
||||
} else if (node.local === MutationKind.Definite) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'Mutate',
|
||||
value: param,
|
||||
});
|
||||
}
|
||||
if (node.transitive === MutationKind.Conditional) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'MutateTransitiveConditionally',
|
||||
value: param,
|
||||
});
|
||||
} else if (node.transitive === MutationKind.Definite) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'MutateTransitive',
|
||||
value: param,
|
||||
});
|
||||
}
|
||||
if (mutated) {
|
||||
param.effect = Effect.Capture;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 2
|
||||
@@ -129,13 +232,21 @@ export function inferMutationAliasingRanges(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
lvalue.effect = Effect.ConditionallyMutate;
|
||||
}
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const operandEffects = new Map<IdentifierId, Effect>();
|
||||
@@ -187,6 +298,11 @@ export function inferMutationAliasingRanges(fn: HIRFunction): void {
|
||||
operandEffects.set(effect.value.identifier.id, Effect.Freeze);
|
||||
break;
|
||||
}
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
effect,
|
||||
@@ -222,132 +338,156 @@ export function inferMutationAliasingRanges(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: similar to alias effect inference state:
|
||||
* - values represent the conceptual values (context vars, things that got Create*-ed)
|
||||
* into which other values are captured
|
||||
* - variables deals with aliases (Assign, Alias, CreateFrom)
|
||||
*
|
||||
* Ex:
|
||||
* `Capture a -> b`:
|
||||
* - find the values represented by `a` (aValues) by looking up a in .variables, then mapping to .values
|
||||
* - find the values represented by `b` (bValues) by looking up b in .variables, then mapping to .values
|
||||
* - add aValues into bValues
|
||||
*/
|
||||
type Node = {
|
||||
id: Identifier;
|
||||
captures: Map<Identifier, number>;
|
||||
aliases: Map<Identifier, number>;
|
||||
edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>;
|
||||
transitive: MutationKind;
|
||||
local: MutationKind;
|
||||
};
|
||||
class AliasingState {
|
||||
values: Map<Identifier, Set<Identifier>> = new Map();
|
||||
variables: Map<IdentifierId, Set<Identifier>> = new Map();
|
||||
nodes: Map<Identifier, Node> = new Map();
|
||||
|
||||
create(place: Place): void {
|
||||
this.values.set(place.identifier, new Set());
|
||||
this.variables.set(place.identifier.id, new Set([place.identifier]));
|
||||
this.nodes.set(place.identifier, {
|
||||
id: place.identifier,
|
||||
captures: new Map(),
|
||||
aliases: new Map(),
|
||||
edges: [],
|
||||
transitive: MutationKind.None,
|
||||
local: MutationKind.None,
|
||||
});
|
||||
}
|
||||
|
||||
clear(lvalue: Place): void {
|
||||
this.variables.set(lvalue.identifier.id, new Set());
|
||||
}
|
||||
|
||||
assign(from: Place, into: Place): void {
|
||||
const fromVariables = this.variables.get(from.identifier.id);
|
||||
if (fromVariables == null) {
|
||||
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 (DEBUG) {
|
||||
console.log(
|
||||
`skip: capture ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.variables.set(
|
||||
into.identifier.id,
|
||||
new Set([...fromVariables, from.identifier, into.identifier]),
|
||||
// fromVariables,
|
||||
);
|
||||
}
|
||||
|
||||
alias(from: Place, into: Place): void {
|
||||
const intoVariables = getOrInsertWith(
|
||||
this.variables,
|
||||
into.identifier.id,
|
||||
() => new Set(),
|
||||
);
|
||||
intoVariables.add(from.identifier);
|
||||
}
|
||||
|
||||
capture(from: Place, into: Place): void {
|
||||
const intoVariables = this.variables.get(into.identifier.id)!;
|
||||
for (const v of intoVariables) {
|
||||
const values = this.values.get(v)!;
|
||||
values.add(from.identifier);
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'capture'});
|
||||
if (!toNode.captures.has(from.identifier)) {
|
||||
toNode.captures.set(from.identifier, index);
|
||||
}
|
||||
}
|
||||
|
||||
mutateTransitive(
|
||||
place: Identifier,
|
||||
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 (DEBUG) {
|
||||
console.log(
|
||||
`skip: assign ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
|
||||
if (!toNode.captures.has(from.identifier)) {
|
||||
toNode.aliases.set(from.identifier, index);
|
||||
}
|
||||
}
|
||||
|
||||
mutate(
|
||||
index: number,
|
||||
start: Place,
|
||||
end: InstructionId,
|
||||
loc: SourceLocation,
|
||||
transitive: boolean,
|
||||
kind: MutationKind,
|
||||
): void {
|
||||
const variables = this.variables.get(place.id);
|
||||
if (variables == null) {
|
||||
return;
|
||||
}
|
||||
for (const value of variables) {
|
||||
value.mutableRange.end = makeInstructionId(
|
||||
Math.max(value.mutableRange.end, end),
|
||||
const seen = new Set<Identifier>();
|
||||
const queue = [start.identifier];
|
||||
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) {
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
`no node! ${printPlace(start)} for identifier ${printIdentifier(current)}`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
`mutate $${node.id.id} via ${printPlace(start)} at [${end}]`,
|
||||
);
|
||||
}
|
||||
node.id.mutableRange.end = makeInstructionId(
|
||||
Math.max(node.id.mutableRange.end, end),
|
||||
);
|
||||
const captured = this.values.get(value)!;
|
||||
for (const capture of captured) {
|
||||
this.mutateTransitive(capture, end, loc);
|
||||
if (transitive) {
|
||||
node.transitive = Math.max(node.transitive, kind);
|
||||
} else {
|
||||
node.local = Math.max(node.local, kind);
|
||||
}
|
||||
/**
|
||||
* 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(edge.node);
|
||||
}
|
||||
/**
|
||||
* all mutations affect backward alias edges by the rules:
|
||||
* - Alias a -> b, mutate(b) => mutate(a)
|
||||
* - Alias a -> b, mutateTransitive(b) => mutate(a)
|
||||
*/
|
||||
for (const [alias, when] of node.aliases) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push(alias);
|
||||
}
|
||||
/**
|
||||
* but only transitive mutations affect captures
|
||||
*/
|
||||
if (transitive) {
|
||||
for (const [capture, when] of node.captures) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push(capture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutate(place: Identifier, end: InstructionId, _loc: SourceLocation): void {
|
||||
const variables = this.variables.get(place.id);
|
||||
if (variables == null) {
|
||||
return;
|
||||
}
|
||||
place.mutableRange.end = makeInstructionId(
|
||||
Math.max(place.mutableRange.end, end),
|
||||
);
|
||||
for (const value of variables) {
|
||||
// Unlike mutateTransitive, we don't traverse into captured values
|
||||
value.mutableRange.end = makeInstructionId(
|
||||
Math.max(value.mutableRange.end, end),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
canonicalize(): Map<IdentifierId, string> {
|
||||
const map = new Map<IdentifierId, string>();
|
||||
for (const value of this.values.keys()) {
|
||||
map.set(
|
||||
value.id,
|
||||
`${value.mutableRange.start}:${value.mutableRange.end}`,
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
debug(): string {
|
||||
const values = new Map<string, string>();
|
||||
for (const [k, v] of this.values) {
|
||||
values.set(
|
||||
`${k.name?.value ?? ''}$${k.id}:${k.mutableRange.start}:${k.mutableRange.end}`,
|
||||
[...v]
|
||||
.map(
|
||||
v =>
|
||||
`${v.name?.value ?? ''}$${v.id}:${v.mutableRange.start}:${v.mutableRange.end}`,
|
||||
)
|
||||
.join(', '),
|
||||
);
|
||||
}
|
||||
const variables = new Map<string, string>();
|
||||
for (const [k, v] of this.variables) {
|
||||
variables.set(
|
||||
`$${k}`,
|
||||
[...v]
|
||||
.map(
|
||||
v =>
|
||||
`${v.name?.value ?? ''}$${v.id}:${v.mutableRange.start}:${v.mutableRange.end}`,
|
||||
)
|
||||
.join(', '),
|
||||
);
|
||||
}
|
||||
return prettyFormat({values, variables});
|
||||
return pretty(this.nodes);
|
||||
}
|
||||
}
|
||||
|
||||
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}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
import {identity, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
|
||||
* with the mutation hoisted to a named variable instead of being directly
|
||||
* inlined into the Object key.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* Forget:
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
|
||||
*/
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const tmp = (mutate(key), key);
|
||||
const context = {
|
||||
// Here, `tmp` is frozen (as it's inferred to be a primitive/string)
|
||||
[tmp]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return [context, key];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [{value: 42}, {value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
import { identity, mutate } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
|
||||
* with the mutation hoisted to a named variable instead of being directly
|
||||
* inlined into the Object key.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* Forget:
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
|
||||
*/
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.value) {
|
||||
const key = {};
|
||||
const tmp = (mutate(key), key);
|
||||
const context = { [tmp]: identity([props.value]) };
|
||||
|
||||
mutate(key);
|
||||
t0 = [context, key];
|
||||
$[0] = props.value;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
sequentialRenders: [{ value: 42 }, { value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
[{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
import {identity, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
|
||||
* with the mutation hoisted to a named variable instead of being directly
|
||||
* inlined into the Object key.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* Forget:
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
|
||||
*/
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const tmp = (mutate(key), key);
|
||||
const context = {
|
||||
// Here, `tmp` is frozen (as it's inferred to be a primitive/string)
|
||||
[tmp]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return [context, key];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [{value: 42}, {value: 42}],
|
||||
};
|
||||
+58
@@ -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
|
||||
+16
@@ -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: {}}],
|
||||
};
|
||||
+50
@@ -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
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
function Component({a, b}) {
|
||||
const x = [a];
|
||||
const y = {b};
|
||||
mutate(y);
|
||||
y.x = x;
|
||||
return <div>{y}</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user