Compare commits
77 Commits
pr33767
...
gh/josephs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2c10efd13 | ||
|
|
38e929cb0c | ||
|
|
1f023ebbbe | ||
|
|
4922869278 | ||
|
|
421571966c | ||
|
|
9e5ce15dfe | ||
|
|
a76977f644 | ||
|
|
4ae090860c | ||
|
|
354d6d88de | ||
|
|
93755d3075 | ||
|
|
7482763995 | ||
|
|
6b561f679d | ||
|
|
852dec879f | ||
|
|
c98c55bf34 | ||
|
|
333fd8d49d | ||
|
|
35ae2fa72d | ||
|
|
c28dc88c0c | ||
|
|
ac2340e479 | ||
|
|
eb8d0c27b0 | ||
|
|
bc01ae2157 | ||
|
|
3def726cac | ||
|
|
fd162bdd9d | ||
|
|
0d6822a1da | ||
|
|
93e24f467e | ||
|
|
c207ff562a | ||
|
|
cdd2d3f2f9 | ||
|
|
51d5aa3f33 | ||
|
|
8c70807cec | ||
|
|
a219df1c98 | ||
|
|
1a733b8946 | ||
|
|
74c236a99d | ||
|
|
f9bb6067ab | ||
|
|
94409c39f3 | ||
|
|
e8d39119b1 | ||
|
|
ff14c671fd | ||
|
|
ca0f0e006d | ||
|
|
05a8b6a717 | ||
|
|
58e449ee0e | ||
|
|
61bd21cd18 | ||
|
|
3e67c7e39f | ||
|
|
6cc26518f3 | ||
|
|
79a13a29c5 | ||
|
|
80ae4e96ae | ||
|
|
f5cd556ffa | ||
|
|
0c7e645dd3 | ||
|
|
b0b09a74a8 | ||
|
|
f40a865bc9 | ||
|
|
bfff07b450 | ||
|
|
5fc610efb3 | ||
|
|
68ab155cfe | ||
|
|
4afa39de4e | ||
|
|
275866f3fc | ||
|
|
45a889cb87 | ||
|
|
23222b4b36 | ||
|
|
98b02b93f9 | ||
|
|
52a34e4fa0 | ||
|
|
5bf3eb6085 | ||
|
|
b9367e0e3b | ||
|
|
90ed7e8228 | ||
|
|
a25a7e1873 | ||
|
|
206e7e93d8 | ||
|
|
63e29f78e3 | ||
|
|
ec552a5011 | ||
|
|
390d516127 | ||
|
|
3b83903ca1 | ||
|
|
1fec32eeeb | ||
|
|
fd0df94b1a | ||
|
|
97442d79cb | ||
|
|
7936a99a2d | ||
|
|
c3a733fe2d | ||
|
|
05e75d8055 | ||
|
|
cd2537c6a6 | ||
|
|
3d646a8993 | ||
|
|
15307de4e2 | ||
|
|
e9b19a6732 | ||
|
|
0ee6891337 | ||
|
|
18e4165a94 |
3
compiler/.gitignore
vendored
3
compiler/.gitignore
vendored
@@ -6,6 +6,9 @@
|
||||
debug/
|
||||
target/
|
||||
|
||||
nocommit*.js
|
||||
nocommit*.expect.md
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
|
||||
@@ -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}
|
||||
@@ -226,15 +228,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);
|
||||
@@ -248,8 +262,13 @@ 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 {
|
||||
inferMutationAliasingRanges(hir);
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
}
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.config.assertValidMutableRanges) {
|
||||
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
makeType,
|
||||
promoteTemporary,
|
||||
} from './HIR';
|
||||
import HIRBuilder, {Bindings} from './HIRBuilder';
|
||||
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
|
||||
import {BuiltInArrayId} from './ObjectShape';
|
||||
|
||||
/*
|
||||
@@ -218,6 +218,7 @@ export function lower(
|
||||
fnType: parent == 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,
|
||||
@@ -225,6 +226,7 @@ export function lower(
|
||||
loc: func.node.loc ?? GeneratedSource,
|
||||
env,
|
||||
effects: null,
|
||||
aliasingEffects: null,
|
||||
directives,
|
||||
});
|
||||
}
|
||||
@@ -1235,6 +1237,7 @@ function lowerStatement(
|
||||
kind: 'Debugger',
|
||||
loc,
|
||||
},
|
||||
effects: null,
|
||||
loc,
|
||||
});
|
||||
return;
|
||||
@@ -1892,6 +1895,7 @@ function lowerExpression(
|
||||
place: leftValue,
|
||||
loc: exprLoc,
|
||||
},
|
||||
effects: null,
|
||||
loc: exprLoc,
|
||||
});
|
||||
builder.terminateWithContinuation(
|
||||
@@ -2827,6 +2831,7 @@ function lowerOptionalCallExpression(
|
||||
args,
|
||||
loc,
|
||||
},
|
||||
effects: null,
|
||||
loc,
|
||||
});
|
||||
} else {
|
||||
@@ -2840,6 +2845,7 @@ function lowerOptionalCallExpression(
|
||||
args,
|
||||
loc,
|
||||
},
|
||||
effects: null,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
@@ -3466,9 +3472,10 @@ 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
|
||||
|
||||
@@ -13,6 +13,7 @@ import {Environment, ReactFunctionType} from './Environment';
|
||||
import type {HookKind} from './ObjectShape';
|
||||
import {Type, makeType} from './Types';
|
||||
import {z} from 'zod';
|
||||
import {AliasingEffect} from '../Inference/InferMutationAliasingEffects';
|
||||
|
||||
/*
|
||||
* *******************************************************************************************
|
||||
@@ -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 =
|
||||
@@ -300,6 +304,10 @@ export type FunctionEffect =
|
||||
places: ReadonlySet<Place>;
|
||||
effect: Effect;
|
||||
loc: SourceLocation;
|
||||
}
|
||||
| {
|
||||
kind: 'CaptureEffect';
|
||||
places: ReadonlySet<Place>;
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -645,12 +653,18 @@ export type Instruction = {
|
||||
lvalue: Place;
|
||||
value: InstructionValue;
|
||||
loc: SourceLocation;
|
||||
effects: Array<AliasingEffect> | null;
|
||||
};
|
||||
|
||||
export function todoPopulateAliasingEffects(): Array<AliasingEffect> | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export type TInstruction<T extends InstructionValue> = {
|
||||
id: InstructionId;
|
||||
lvalue: Place;
|
||||
value: T;
|
||||
effects: Array<AliasingEffect> | null;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
|
||||
@@ -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/InferMutationAliasingEffects';
|
||||
import {
|
||||
Effect,
|
||||
GeneratedSource,
|
||||
makeDeclarationId,
|
||||
makeIdentifierId,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
ValueKind,
|
||||
ValueReason,
|
||||
} from './HIR';
|
||||
import {
|
||||
BuiltInType,
|
||||
FunctionType,
|
||||
makeType,
|
||||
ObjectType,
|
||||
PolyType,
|
||||
PrimitiveType,
|
||||
@@ -179,6 +190,8 @@ export type FunctionSignature = {
|
||||
impure?: boolean;
|
||||
|
||||
canonicalName?: string;
|
||||
|
||||
aliasing?: AliasingSignature | null;
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -302,6 +315,28 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Store,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
aliasing: {
|
||||
receiver: makeIdentifierId(0),
|
||||
params: [],
|
||||
rest: makeIdentifierId(1),
|
||||
returns: makeIdentifierId(2),
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -332,6 +367,7 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
noAlias: true,
|
||||
mutableOnlyIfOperandsAreMutable: true,
|
||||
aliasing: null,
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -1169,3 +1205,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,11 @@ import type {
|
||||
Type,
|
||||
} from './HIR';
|
||||
import {GotoVariant, InstructionKind} from './HIR';
|
||||
import {
|
||||
AliasedPlace,
|
||||
AliasingEffect,
|
||||
AliasingSignature,
|
||||
} from '../Inference/InferMutationAliasingEffects';
|
||||
|
||||
export type Options = {
|
||||
indent: number;
|
||||
@@ -67,13 +72,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 +158,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}`;
|
||||
@@ -546,17 +556,31 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
const effects =
|
||||
instrValue.loweredFunc.func.effects
|
||||
?.map(effect => {
|
||||
if (effect.kind === 'ContextMutation') {
|
||||
return `ContextMutation places=[${[...effect.places]
|
||||
.map(place => printPlace(place))
|
||||
.join(', ')}] effect=${effect.effect}`;
|
||||
} else {
|
||||
return `GlobalMutation`;
|
||||
switch (effect.kind) {
|
||||
case 'ContextMutation': {
|
||||
return `ContextMutation places=[${[...effect.places]
|
||||
.map(place => printPlace(place))
|
||||
.join(', ')}] effect=${effect.effect}`;
|
||||
}
|
||||
case 'GlobalMutation': {
|
||||
return 'GlobalMutation';
|
||||
}
|
||||
case 'ReactMutation': {
|
||||
return 'ReactMutation';
|
||||
}
|
||||
case 'CaptureEffect': {
|
||||
return `CaptureEffect places=[${[...effect.places]
|
||||
.map(place => printPlace(place))
|
||||
.join(', ')}]`;
|
||||
}
|
||||
}
|
||||
})
|
||||
.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': {
|
||||
@@ -720,7 +744,7 @@ function isMutable(range: MutableRange): boolean {
|
||||
}
|
||||
|
||||
const DEBUG_MUTABLE_RANGES = false;
|
||||
function printMutableRange(identifier: Identifier): string {
|
||||
export function printMutableRange(identifier: Identifier): string {
|
||||
if (DEBUG_MUTABLE_RANGES) {
|
||||
// if debugging, print both the identifier and scope range if they differ
|
||||
const range = identifier.mutableRange;
|
||||
@@ -922,3 +946,67 @@ function getFunctionName(
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
export function printAliasingEffect(effect: AliasingEffect): string {
|
||||
switch (effect.kind) {
|
||||
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 'Apply': {
|
||||
const params = effect.params.map(printAliasedPlace).join(', ');
|
||||
const rest = effect.rest != null ? printAliasedPlace(effect.rest) : '';
|
||||
const returns = printAliasedPlace(effect.returns);
|
||||
return `Apply ${returns} = ${printAliasedPlace(effect.function)} as ${effect.receiver} ( ${params} ${params.length !== 0 && rest !== '' ? ', ...' : ''}${rest})`;
|
||||
}
|
||||
case 'Freeze': {
|
||||
return `Freeze ${printPlaceForAliasEffect(effect.value)} ${effect.reason}`;
|
||||
}
|
||||
case 'Mutate':
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitive':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(effect, `Unexpected kind '${(effect as any).kind}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printPlaceForAliasEffect(place: Place): string {
|
||||
return printIdentifier(place.identifier);
|
||||
}
|
||||
|
||||
function printAliasedPlace(place: AliasedPlace): string {
|
||||
return place.kind + ' ' + printPlaceForAliasEffect(place.place);
|
||||
}
|
||||
|
||||
export function printAliasingSignature(signature: AliasingSignature): string {
|
||||
const tokens: Array<string> = ['function '];
|
||||
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('');
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
LoweredFunction,
|
||||
Place,
|
||||
isRefOrRefValue,
|
||||
makeInstructionId,
|
||||
} from '../HIR';
|
||||
@@ -19,6 +21,15 @@ import {inferReactiveScopeVariables} from '../ReactiveScopes';
|
||||
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
|
||||
import {inferMutableRanges} from './InferMutableRanges';
|
||||
import inferReferenceEffects from './InferReferenceEffects';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive, Iterable_some} 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 +37,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) {
|
||||
const aliases = lower(instr.value.loweredFunc.func);
|
||||
infer(instr.value.loweredFunc, aliases);
|
||||
} else {
|
||||
lowerWithMutationAliasing(instr.value.loweredFunc.func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset mutable range for outer inferReferenceEffects
|
||||
@@ -44,11 +59,70 @@ export default function analyseFunctions(func: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
|
||||
function lower(func: HIRFunction): void {
|
||||
function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
analyseFunctions(fn);
|
||||
inferMutationAliasingEffects(fn, {isFunctionExpression: true});
|
||||
deadCodeElimination(fn);
|
||||
inferMutationAliasingRanges(fn);
|
||||
rewriteInstructionKindsBasedOnReassignment(fn);
|
||||
inferReactiveScopeVariables(fn);
|
||||
fn.env.logger?.debugLogIRs?.({
|
||||
kind: 'hir',
|
||||
name: 'AnalyseFunction (inner)',
|
||||
value: fn,
|
||||
});
|
||||
const effects = inferMutationAliasingFunctionEffects(fn);
|
||||
fn.aliasingEffects = effects;
|
||||
|
||||
const capturedOrMutated = new Set<IdentifierId>();
|
||||
for (const effect of effects ?? []) {
|
||||
switch (effect.kind) {
|
||||
case 'Alias':
|
||||
case 'Capture':
|
||||
case 'CreateFrom': {
|
||||
capturedOrMutated.add(effect.from.identifier.id);
|
||||
break;
|
||||
}
|
||||
case 'Apply': {
|
||||
capturedOrMutated.add(effect.function.place.identifier.id);
|
||||
break;
|
||||
}
|
||||
case 'Mutate':
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitive':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
capturedOrMutated.add(effect.value.identifier.id);
|
||||
break;
|
||||
}
|
||||
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;
|
||||
} else {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lower(func: HIRFunction): DisjointSet<Identifier> {
|
||||
analyseFunctions(func);
|
||||
inferReferenceEffects(func, {isFunctionExpression: true});
|
||||
deadCodeElimination(func);
|
||||
inferMutableRanges(func);
|
||||
const aliases = inferMutableRanges(func);
|
||||
rewriteInstructionKindsBasedOnReassignment(func);
|
||||
inferReactiveScopeVariables(func);
|
||||
func.env.logger?.debugLogIRs?.({
|
||||
@@ -56,9 +130,61 @@ function lower(func: HIRFunction): void {
|
||||
name: 'AnalyseFunction (inner)',
|
||||
value: func,
|
||||
});
|
||||
inferAliasesForCapturing(func, aliases);
|
||||
return aliases ?? new DisjointSet();
|
||||
}
|
||||
|
||||
function infer(loweredFunc: LoweredFunction): void {
|
||||
/**
|
||||
* The alias sets returned by InferMutableRanges() accounts only for aliases that
|
||||
* are known to mutate together. Notably this skips cases where a value is captured
|
||||
* into some other value, but neither is subsequently mutated. An example is pushing
|
||||
* a mutable value onto an array, where neither the array or value are subsequently
|
||||
* mutated.
|
||||
*
|
||||
* This function extends the aliases sets to account for such capturing, so that we
|
||||
* can detect cases where one of the values in a set is mutated later (in an outer function)
|
||||
* we can correctly infer them as mutating together.
|
||||
*/
|
||||
function inferAliasesForCapturing(
|
||||
fn: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
const hasCapture =
|
||||
lvalue.effect === Effect.Store ||
|
||||
Iterable_some(
|
||||
eachInstructionValueOperand(value),
|
||||
operand => operand.effect === Effect.Capture,
|
||||
);
|
||||
if (!hasCapture) {
|
||||
continue;
|
||||
}
|
||||
const operands: Array<Identifier> = [];
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
operands.push(lvalue.identifier);
|
||||
}
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
if (
|
||||
operand.effect === Effect.Store ||
|
||||
operand.effect === Effect.Mutate ||
|
||||
operand.effect === Effect.Capture
|
||||
) {
|
||||
operands.push(operand.identifier);
|
||||
}
|
||||
}
|
||||
if (operands.length > 1) {
|
||||
aliases.union(operands);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function infer(
|
||||
loweredFunc: LoweredFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
for (const operand of loweredFunc.func.context) {
|
||||
const identifier = operand.identifier;
|
||||
CompilerError.invariant(operand.effect === Effect.Unknown, {
|
||||
@@ -85,6 +211,23 @@ function infer(loweredFunc: LoweredFunction): void {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
const contextIdentifiers = new Map(
|
||||
loweredFunc.func.context.map(place => [place.identifier, place]),
|
||||
);
|
||||
for (const set of aliases.buildSets()) {
|
||||
const contextOperands: Set<Place> = new Set(
|
||||
[...set]
|
||||
.map(identifier => contextIdentifiers.get(identifier))
|
||||
.filter(place => place != null) as Array<Place>,
|
||||
);
|
||||
if (contextOperands.size !== 0) {
|
||||
loweredFunc.func.effects ??= [];
|
||||
loweredFunc.func.effects?.push({
|
||||
kind: 'CaptureEffect',
|
||||
places: contextOperands,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isMutatedOrReassigned(id: Identifier): boolean {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -60,6 +60,10 @@ function inferInstr(
|
||||
alias = instrValue.value;
|
||||
break;
|
||||
}
|
||||
case 'IteratorNext': {
|
||||
alias = instrValue.collection;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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, Identifier} from '../HIR/HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export function inferAliasForFunctionCaptureEffects(
|
||||
func: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
const {value} = instr;
|
||||
if (
|
||||
value.kind !== 'FunctionExpression' &&
|
||||
value.kind !== 'ObjectMethod'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const loweredFunction = value.loweredFunc.func;
|
||||
for (const effect of loweredFunction.effects ?? []) {
|
||||
if (effect.kind !== 'CaptureEffect') {
|
||||
continue;
|
||||
}
|
||||
aliases.union([...effect.places].map(place => place.identifier));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
isSetStateType,
|
||||
isFireFunctionType,
|
||||
makeScopeId,
|
||||
todoPopulateAliasingEffects,
|
||||
} from '../HIR';
|
||||
import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads';
|
||||
import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies';
|
||||
@@ -236,9 +237,10 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
|
||||
newInstructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
value: deps,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
|
||||
// Step 2: push the inferred deps array as an argument of the useEffect
|
||||
@@ -249,9 +251,10 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
// Global functions have no reactive dependencies, so we can insert an empty array
|
||||
newInstructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
value: deps,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
value.args.push({...depsPlace, effect: Effect.Freeze});
|
||||
rewriteInstrs.set(instr.id, newInstructions);
|
||||
@@ -316,21 +319,25 @@ function writeDependencyToInstructions(
|
||||
const instructions: Array<Instruction> = [];
|
||||
let currValue = createTemporaryPlace(env, GeneratedSource);
|
||||
currValue.reactive = reactive;
|
||||
const dependencyPlace: Place = {
|
||||
kind: 'Identifier',
|
||||
identifier: dep.identifier,
|
||||
effect: Effect.Capture,
|
||||
reactive,
|
||||
loc: loc,
|
||||
};
|
||||
instructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...currValue, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'LoadLocal',
|
||||
place: {
|
||||
kind: 'Identifier',
|
||||
identifier: dep.identifier,
|
||||
effect: Effect.Capture,
|
||||
reactive,
|
||||
loc: loc,
|
||||
},
|
||||
place: {...dependencyPlace},
|
||||
loc: loc,
|
||||
},
|
||||
effects: [
|
||||
{kind: 'Alias', from: {...dependencyPlace}, into: {...currValue}},
|
||||
],
|
||||
});
|
||||
for (const path of dep.path) {
|
||||
if (path.optional) {
|
||||
@@ -359,6 +366,7 @@ function writeDependencyToInstructions(
|
||||
property: path.property,
|
||||
loc: loc,
|
||||
},
|
||||
effects: [{kind: 'Capture', from: {...currValue}, into: {...nextValue}}],
|
||||
});
|
||||
currValue = nextValue;
|
||||
}
|
||||
|
||||
@@ -95,45 +95,58 @@ function inheritFunctionEffects(
|
||||
|
||||
return effects
|
||||
.flatMap(effect => {
|
||||
if (effect.kind === 'GlobalMutation' || effect.kind === 'ReactMutation') {
|
||||
return [effect];
|
||||
} else {
|
||||
const effects: Array<FunctionEffect | null> = [];
|
||||
CompilerError.invariant(effect.kind === 'ContextMutation', {
|
||||
reason: 'Expected ContextMutation',
|
||||
loc: null,
|
||||
});
|
||||
/**
|
||||
* Contextual effects need to be replayed against the current inference
|
||||
* state, which may know more about the value to which the effect applied.
|
||||
* The main cases are:
|
||||
* 1. The mutated context value is _still_ a context value in the current scope,
|
||||
* so we have to continue propagating the original context mutation.
|
||||
* 2. The mutated context value is a mutable value in the current scope,
|
||||
* so the context mutation was fine and we can skip propagating the effect.
|
||||
* 3. The mutated context value is an immutable value in the current scope,
|
||||
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
|
||||
* more detailed effect to the current function context.
|
||||
*/
|
||||
for (const place of effect.places) {
|
||||
if (state.isDefined(place)) {
|
||||
const replayedEffect = inferOperandEffect(state, {
|
||||
...place,
|
||||
loc: effect.loc,
|
||||
effect: effect.effect,
|
||||
});
|
||||
if (replayedEffect != null) {
|
||||
if (replayedEffect.kind === 'ContextMutation') {
|
||||
// Case 1, still a context value so propagate the original effect
|
||||
effects.push(effect);
|
||||
} else {
|
||||
// Case 3, immutable value so propagate the more precise effect
|
||||
effects.push(replayedEffect);
|
||||
}
|
||||
} // else case 2, local mutable value so this effect was fine
|
||||
}
|
||||
switch (effect.kind) {
|
||||
case 'GlobalMutation':
|
||||
case 'ReactMutation': {
|
||||
return [effect];
|
||||
}
|
||||
case 'ContextMutation': {
|
||||
const effects: Array<FunctionEffect | null> = [];
|
||||
CompilerError.invariant(effect.kind === 'ContextMutation', {
|
||||
reason: 'Expected ContextMutation',
|
||||
loc: null,
|
||||
});
|
||||
/**
|
||||
* Contextual effects need to be replayed against the current inference
|
||||
* state, which may know more about the value to which the effect applied.
|
||||
* The main cases are:
|
||||
* 1. The mutated context value is _still_ a context value in the current scope,
|
||||
* so we have to continue propagating the original context mutation.
|
||||
* 2. The mutated context value is a mutable value in the current scope,
|
||||
* so the context mutation was fine and we can skip propagating the effect.
|
||||
* 3. The mutated context value is an immutable value in the current scope,
|
||||
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
|
||||
* more detailed effect to the current function context.
|
||||
*/
|
||||
for (const place of effect.places) {
|
||||
if (state.isDefined(place)) {
|
||||
const replayedEffect = inferOperandEffect(state, {
|
||||
...place,
|
||||
loc: effect.loc,
|
||||
effect: effect.effect,
|
||||
});
|
||||
if (replayedEffect != null) {
|
||||
if (replayedEffect.kind === 'ContextMutation') {
|
||||
// Case 1, still a context value so propagate the original effect
|
||||
effects.push(effect);
|
||||
} else {
|
||||
// Case 3, immutable value so propagate the more precise effect
|
||||
effects.push(replayedEffect);
|
||||
}
|
||||
} // else case 2, local mutable value so this effect was fine
|
||||
}
|
||||
}
|
||||
return effects;
|
||||
}
|
||||
case 'CaptureEffect': {
|
||||
return [];
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
effect,
|
||||
`Unexpected effect kind '${(effect as any).kind}'`,
|
||||
);
|
||||
}
|
||||
return effects;
|
||||
}
|
||||
})
|
||||
.filter((effect): effect is FunctionEffect => effect != null);
|
||||
@@ -298,33 +311,38 @@ export function inferTerminalFunctionEffects(
|
||||
export function transformFunctionEffectErrors(
|
||||
functionEffects: Array<FunctionEffect>,
|
||||
): Array<CompilerErrorDetailOptions> {
|
||||
return functionEffects.map(eff => {
|
||||
switch (eff.kind) {
|
||||
case 'ReactMutation':
|
||||
case 'GlobalMutation': {
|
||||
return eff.error;
|
||||
return functionEffects
|
||||
.map(eff => {
|
||||
switch (eff.kind) {
|
||||
case 'ReactMutation':
|
||||
case 'GlobalMutation': {
|
||||
return eff.error;
|
||||
}
|
||||
case 'ContextMutation': {
|
||||
return {
|
||||
severity: ErrorSeverity.Invariant,
|
||||
reason: `Unexpected ContextMutation in top-level function effects`,
|
||||
loc: eff.loc,
|
||||
};
|
||||
}
|
||||
case 'CaptureEffect': {
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
assertExhaustive(
|
||||
eff,
|
||||
`Unexpected function effect kind \`${(eff as any).kind}\``,
|
||||
);
|
||||
}
|
||||
case 'ContextMutation': {
|
||||
return {
|
||||
severity: ErrorSeverity.Invariant,
|
||||
reason: `Unexpected ContextMutation in top-level function effects`,
|
||||
loc: eff.loc,
|
||||
};
|
||||
}
|
||||
default:
|
||||
assertExhaustive(
|
||||
eff,
|
||||
`Unexpected function effect kind \`${(eff as any).kind}\``,
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
.filter(eff => eff != null) as Array<CompilerErrorDetailOptions>;
|
||||
}
|
||||
|
||||
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)) {
|
||||
|
||||
@@ -5,16 +5,21 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {HIRFunction, Identifier} from '../HIR/HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
|
||||
import {inferAliases} from './InferAlias';
|
||||
import {inferAliasForFunctionCaptureEffects} from './InferAliasesForFunctionCaptureEffects';
|
||||
import {inferAliasForPhis} from './InferAliasForPhis';
|
||||
import {inferAliasForStores} from './InferAliasForStores';
|
||||
import {inferMutableLifetimes} from './InferMutableLifetimes';
|
||||
import {inferMutableRangesForAlias} from './InferMutableRangesForAlias';
|
||||
import {inferMutableRangesForComutation} from './InferMutableRangesForComutation';
|
||||
import {inferTryCatchAliases} from './InferTryCatchAliases';
|
||||
import {printIdentifier} from '../HIR/PrintHIR';
|
||||
|
||||
export function inferMutableRanges(ir: HIRFunction): void {
|
||||
export function inferMutableRanges(ir: HIRFunction): DisjointSet<Identifier> {
|
||||
// Infer mutable ranges for non fields
|
||||
inferMutableLifetimes(ir, false);
|
||||
|
||||
@@ -30,18 +35,22 @@ export function inferMutableRanges(ir: HIRFunction): void {
|
||||
* Eagerly canonicalize so that if nothing changes we can bail out
|
||||
* after a single iteration
|
||||
*/
|
||||
let prevAliases: Map<Identifier, Identifier> = aliases.canonicalize();
|
||||
let prevAliases: Map<Identifier, string> = canonicalize(aliases);
|
||||
while (true) {
|
||||
// Infer mutable ranges for aliases that are not fields
|
||||
inferMutableRangesForAlias(ir, aliases);
|
||||
|
||||
inferMutableRangesForComutation(ir);
|
||||
|
||||
// Update aliasing information of fields
|
||||
inferAliasForStores(ir, aliases);
|
||||
|
||||
inferAliasForFunctionCaptureEffects(ir, aliases);
|
||||
|
||||
// Update aliasing information of phis
|
||||
inferAliasForPhis(ir, aliases);
|
||||
|
||||
const nextAliases = aliases.canonicalize();
|
||||
const nextAliases = canonicalize(aliases);
|
||||
if (areEqualMaps(prevAliases, nextAliases)) {
|
||||
break;
|
||||
}
|
||||
@@ -73,20 +82,58 @@ export function inferMutableRanges(ir: HIRFunction): void {
|
||||
* but does not modify values that `y` "contains" such as the
|
||||
* object literal or `z`.
|
||||
*/
|
||||
prevAliases = aliases.canonicalize();
|
||||
prevAliases = canonicalize(aliases);
|
||||
while (true) {
|
||||
inferMutableRangesForAlias(ir, aliases);
|
||||
inferMutableRangesForComutation(ir);
|
||||
inferAliasForPhis(ir, aliases);
|
||||
inferAliasForUncalledFunctions(ir, aliases);
|
||||
const nextAliases = aliases.canonicalize();
|
||||
const nextAliases = canonicalize(aliases);
|
||||
if (areEqualMaps(prevAliases, nextAliases)) {
|
||||
break;
|
||||
}
|
||||
prevAliases = nextAliases;
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
|
||||
function areEqualMaps<T>(a: Map<T, T>, b: Map<T, T>): boolean {
|
||||
export function debugAliases(aliases: DisjointSet<Identifier>): void {
|
||||
console.log(
|
||||
prettyFormat(
|
||||
aliases
|
||||
.buildSets()
|
||||
.map(set =>
|
||||
[...set].map(
|
||||
ident =>
|
||||
`${printIdentifier(ident)}:${ident.mutableRange.start}:${ident.mutableRange.end}`,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonicalizes the alias set and mutable range information calculated at the current time.
|
||||
* The returned value maps each identifier in the program to the root identifier of its alias
|
||||
* set and the the mutable range of that set.
|
||||
*
|
||||
* This ensures that we fixpoint the mutable ranges themselves and not just the alias sets.
|
||||
*/
|
||||
export function canonicalize(
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): Map<Identifier, string> {
|
||||
const entries = new Map<Identifier, string>();
|
||||
aliases.forEach((item, root) => {
|
||||
entries.set(
|
||||
item,
|
||||
`${root.id}:${root.mutableRange.start}:${root.mutableRange.end}`,
|
||||
);
|
||||
});
|
||||
return entries;
|
||||
}
|
||||
|
||||
export function areEqualMaps<T, U>(a: Map<T, U>, b: Map<T, U>): boolean {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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,
|
||||
Identifier,
|
||||
isRefOrRefValue,
|
||||
makeInstructionId,
|
||||
} from '../HIR';
|
||||
import {eachInstructionOperand} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
|
||||
/**
|
||||
* Finds instructions with operands that co-mutate and extends all their mutable ranges
|
||||
* to end at the same point (the highest `end` value of the group). Note that the
|
||||
* alias sets used in InferMutableRanges are meant for values that strictly alias:
|
||||
* a mutation of one value in the set would directly modify the same object as some
|
||||
* other value in the set.
|
||||
*
|
||||
* However, co-mutation can cause an alias to one object to be stored within another object,
|
||||
* for example:
|
||||
*
|
||||
* ```
|
||||
* const a = {};
|
||||
* const b = {};
|
||||
* const f = () => b.c; //
|
||||
* setProperty(a, 'b', b); // equiv to a.b = b
|
||||
*
|
||||
* a.b.c = 'c'; // this mutates b!
|
||||
* ```
|
||||
*
|
||||
* Here, the co-mutation in `setProperty(a, 'b', b)` means that a reference to b may be stored
|
||||
* in a, vice-versa, or both. We need to extend the mutable range of both a and b to reflect
|
||||
* the fact the values may mutate together.
|
||||
*
|
||||
* Previously this was implemented in InferReactiveScopeVariables, but that is too late:
|
||||
* we need this to be part of the InferMutableRanges fixpoint iteration to account for functions
|
||||
* like `f` in the example, which capture a reference to a value that may change later. `f`
|
||||
* cannot be independently memoized from the `setProperty()` call due to the co-mutation.
|
||||
*
|
||||
* See aliased-capture-mutate and aliased-capture-aliased-mutate fixtures for examples.
|
||||
*/
|
||||
export function inferMutableRangesForComutation(fn: HIRFunction): void {
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
let operands: Array<Identifier> | null = null;
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
if (
|
||||
isMutable(instr, operand) &&
|
||||
operand.identifier.mutableRange.start > 0
|
||||
) {
|
||||
if (
|
||||
instr.value.kind === 'FunctionExpression' ||
|
||||
instr.value.kind === 'ObjectMethod'
|
||||
) {
|
||||
if (operand.identifier.type.kind === 'Primitive') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
operands ??= [];
|
||||
operands.push(operand.identifier);
|
||||
}
|
||||
}
|
||||
if (operands != null) {
|
||||
// Find the last instruction which mutates any of the mutable operands
|
||||
let lastMutatingInstructionId = makeInstructionId(0);
|
||||
for (const id of operands) {
|
||||
if (id.mutableRange.end > lastMutatingInstructionId) {
|
||||
lastMutatingInstructionId = id.mutableRange.end;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all mutable operands's mutable ranges to end at the same point
|
||||
*/
|
||||
for (const id of operands) {
|
||||
if (
|
||||
id.mutableRange.end < lastMutatingInstructionId &&
|
||||
!isRefOrRefValue(id)
|
||||
) {
|
||||
id.mutableRange.end = lastMutatingInstructionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* 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, ScopeId} from '../HIR';
|
||||
import {getOrInsertDefault} from '../Utils/utils';
|
||||
import {AliasingEffect} from './InferMutationAliasingEffects';
|
||||
|
||||
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 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
|
||||
* 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: 'Capture' | 'Alias' | 'CreateFrom';
|
||||
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 === '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 === 'Mutate' &&
|
||||
effect.value.identifier.scope != null &&
|
||||
trackedScopes.has(effect.value.identifier.scope.id)
|
||||
) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (block.terminal.kind === 'return') {
|
||||
const from = lookup(block.terminal.value, 'Alias');
|
||||
if (from != null) {
|
||||
getOrInsertDefault(dataFlow, fn.returns.identifier.id, []).push(
|
||||
...from,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
for (const [into, from] of dataFlow) {
|
||||
const input = tracked.get(into);
|
||||
if (input == null) {
|
||||
continue;
|
||||
}
|
||||
for (const aliased of from) {
|
||||
const effect = {kind: aliased.kind, from: aliased.place, into: input};
|
||||
effects.push(effect);
|
||||
}
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
|
||||
enum MutationKind {
|
||||
None = 0,
|
||||
Conditional = 1,
|
||||
Definite = 2,
|
||||
}
|
||||
|
||||
type AliasingKind = 'Alias' | 'Capture' | 'CreateFrom';
|
||||
function joinEffects(
|
||||
effect1: AliasingKind,
|
||||
effect2: AliasingKind,
|
||||
): AliasingKind {
|
||||
if (effect1 === 'Capture' || effect2 === 'Capture') {
|
||||
return 'Capture';
|
||||
} else {
|
||||
return 'Alias';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* 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 {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
makeInstructionId,
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {inferMutableRangesForAlias} from './InferMutableRangesForAlias';
|
||||
|
||||
/**
|
||||
* Infers mutable ranges for all values.
|
||||
*/
|
||||
export function inferMutationAliasingRanges(fn: HIRFunction): void {
|
||||
/**
|
||||
* Part 1
|
||||
* Infer ranges for transitive mutations, which includes mutations that affect
|
||||
* captured references and not just direct aliases. We build a distjoing set
|
||||
* that tracks capturing and direct aliasing, and look at transitive mutations
|
||||
* only.
|
||||
*/
|
||||
const captures = new DisjointSet<Identifier>();
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
captures.union([
|
||||
phi.place.identifier,
|
||||
...[...phi.operands.values()].map(place => place.identifier),
|
||||
]);
|
||||
}
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
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 === 'Alias' ||
|
||||
effect.kind === 'CreateFrom' ||
|
||||
effect.kind === 'Capture'
|
||||
) {
|
||||
captures.union([effect.from.identifier, effect.into.identifier]);
|
||||
} else if (
|
||||
effect.kind === 'MutateTransitive' ||
|
||||
effect.kind === 'MutateTransitiveConditionally'
|
||||
) {
|
||||
const value = effect.value;
|
||||
value.identifier.mutableRange.end = makeInstructionId(instr.id + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* TODO: this will incorrectly mark values as mutated in the following case:
|
||||
* 1. Create value x
|
||||
* 2. Create value y
|
||||
* 3. Transitively mutate y
|
||||
* 4. Capture x -> y
|
||||
*
|
||||
* We will put these all into one alias set in captures, and then InferMutableRangesForAlias
|
||||
* will look at all identifiers in the set that start before the mutation. Thus it will see
|
||||
* that x is created before the mutation, and consider it mutated. But the capture doesn't
|
||||
* occur until after the mutation!
|
||||
*
|
||||
* The fix is to use a graph to precisely describe what is captured where at each instruction,
|
||||
* so that on a transitive mutation we can iterate all the captured values and mark them.
|
||||
*
|
||||
* This then needs a fixpoint: although we would track captures for phi operands, the operands'
|
||||
* own capture values won't be populated until we do a fixpoint.
|
||||
*/
|
||||
inferMutableRangesForAlias(fn, captures);
|
||||
|
||||
/**
|
||||
* Part 2
|
||||
* Infer ranges for local (non-transitive) mutations. We build a disjoint set
|
||||
* that only tracks direct value aliasing, and look only at local mutations
|
||||
* to extend ranges
|
||||
*
|
||||
* TODO: similar design to the above todo for captures, use a directed graph instead of disjoint union,
|
||||
* with fixpoint.
|
||||
*/
|
||||
const aliases = new DisjointSet<Identifier>();
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
aliases.union([
|
||||
phi.place.identifier,
|
||||
...[...phi.operands.values()].map(place => place.identifier),
|
||||
]);
|
||||
}
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
if (instr.effects == null) continue;
|
||||
for (const effect of instr.effects) {
|
||||
if (effect.kind === 'Alias' || effect.kind === 'CreateFrom') {
|
||||
aliases.union([effect.from.identifier, effect.into.identifier]);
|
||||
} else if (
|
||||
effect.kind === 'Mutate' ||
|
||||
effect.kind === 'MutateConditionally'
|
||||
) {
|
||||
const value = effect.value;
|
||||
value.identifier.mutableRange.end = makeInstructionId(instr.id + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
inferMutableRangesForAlias(fn, aliases);
|
||||
|
||||
/**
|
||||
* Part 3
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
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>();
|
||||
for (const effect of instr.effects) {
|
||||
switch (effect.kind) {
|
||||
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 'ImmutableCapture': {
|
||||
operandEffects.set(effect.from.identifier.id, Effect.Read);
|
||||
break;
|
||||
}
|
||||
case 'Create': {
|
||||
break;
|
||||
}
|
||||
case 'Mutate': {
|
||||
operandEffects.set(effect.value.identifier.id, Effect.Store);
|
||||
break;
|
||||
}
|
||||
case 'Apply': {
|
||||
operandEffects.set(
|
||||
effect.function.place.identifier.id,
|
||||
Effect.ConditionallyMutate,
|
||||
);
|
||||
break;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,13 +31,13 @@ import {
|
||||
isArrayType,
|
||||
isMapType,
|
||||
isMutableEffect,
|
||||
isObjectType,
|
||||
isSetType,
|
||||
isObjectType,
|
||||
} from '../HIR/HIR';
|
||||
import {FunctionSignature} from '../HIR/ObjectShape';
|
||||
import {
|
||||
printIdentifier,
|
||||
printMixedHIR,
|
||||
printInstructionValue,
|
||||
printPlace,
|
||||
printSourceLocation,
|
||||
} from '../HIR/PrintHIR';
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
eachTerminalOperand,
|
||||
eachTerminalSuccessor,
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {assertExhaustive, retainWhere, Set_isSuperset} from '../Utils/utils';
|
||||
import {
|
||||
inferTerminalFunctionEffects,
|
||||
inferInstructionFunctionEffects,
|
||||
@@ -521,7 +521,7 @@ class InferenceState {
|
||||
* `expected valueKind to be 'Mutable' but found to be \`${valueKind}\``
|
||||
* );
|
||||
*/
|
||||
effect = isObjectType(place.identifier) ? Effect.Store : Effect.Mutate;
|
||||
effect = Effect.Store;
|
||||
break;
|
||||
}
|
||||
case Effect.Capture: {
|
||||
@@ -669,7 +669,10 @@ class InferenceState {
|
||||
}
|
||||
for (const [value, kind] of this.#values) {
|
||||
const id = identify(value);
|
||||
result.values[id] = {kind, value: printMixedHIR(value)};
|
||||
result.values[id] = {
|
||||
abstract: this.debugAbstractValue(kind),
|
||||
value: printInstructionValue(value),
|
||||
};
|
||||
}
|
||||
for (const [variable, values] of this.#variables) {
|
||||
result.variables[`$${variable}`] = [...values].map(identify);
|
||||
@@ -677,6 +680,14 @@ class InferenceState {
|
||||
return result;
|
||||
}
|
||||
|
||||
debugAbstractValue(value: AbstractValue): any {
|
||||
return {
|
||||
kind: value.kind,
|
||||
context: [...value.context].map(printPlace),
|
||||
reason: [...value.reason],
|
||||
};
|
||||
}
|
||||
|
||||
inferPhi(phi: Phi): void {
|
||||
const values: Set<InstructionValue> = new Set();
|
||||
for (const [_, operand] of phi.operands) {
|
||||
@@ -779,7 +790,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 +832,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;
|
||||
}
|
||||
@@ -902,19 +901,11 @@ function inferBlock(
|
||||
break;
|
||||
}
|
||||
case 'ArrayExpression': {
|
||||
const contextRefOperands = getContextRefOperand(state, instrValue);
|
||||
const valueKind: AbstractValue =
|
||||
contextRefOperands.length > 0
|
||||
? {
|
||||
kind: ValueKind.Context,
|
||||
reason: new Set([ValueReason.Other]),
|
||||
context: new Set(contextRefOperands),
|
||||
}
|
||||
: {
|
||||
kind: ValueKind.Mutable,
|
||||
reason: new Set([ValueReason.Other]),
|
||||
context: new Set(),
|
||||
};
|
||||
const valueKind: AbstractValue = {
|
||||
kind: ValueKind.Mutable,
|
||||
reason: new Set([ValueReason.Other]),
|
||||
context: new Set(),
|
||||
};
|
||||
|
||||
for (const element of instrValue.elements) {
|
||||
if (element.kind === 'Spread') {
|
||||
@@ -935,6 +926,7 @@ function inferBlock(
|
||||
let _: 'Hole' = element.kind;
|
||||
}
|
||||
}
|
||||
|
||||
state.initialize(instrValue, valueKind);
|
||||
state.define(instr.lvalue, instrValue);
|
||||
instr.lvalue.effect = Effect.Store;
|
||||
@@ -954,19 +946,11 @@ function inferBlock(
|
||||
break;
|
||||
}
|
||||
case 'ObjectExpression': {
|
||||
const contextRefOperands = getContextRefOperand(state, instrValue);
|
||||
const valueKind: AbstractValue =
|
||||
contextRefOperands.length > 0
|
||||
? {
|
||||
kind: ValueKind.Context,
|
||||
reason: new Set([ValueReason.Other]),
|
||||
context: new Set(contextRefOperands),
|
||||
}
|
||||
: {
|
||||
kind: ValueKind.Mutable,
|
||||
reason: new Set([ValueReason.Other]),
|
||||
context: new Set(),
|
||||
};
|
||||
const valueKind: AbstractValue = {
|
||||
kind: ValueKind.Mutable,
|
||||
reason: new Set([ValueReason.Other]),
|
||||
context: new Set(),
|
||||
};
|
||||
|
||||
for (const property of instrValue.properties) {
|
||||
switch (property.kind) {
|
||||
@@ -1190,6 +1174,35 @@ function inferBlock(
|
||||
);
|
||||
hasMutableOperand ||= isMutableEffect(operand.effect, operand.loc);
|
||||
}
|
||||
|
||||
/*
|
||||
* Filter CaptureEffects to remove values that are immutable and don't
|
||||
* need to be tracked for aliasing
|
||||
*/
|
||||
const effects = instrValue.loweredFunc.func.effects;
|
||||
if (effects != null && effects.length !== 0) {
|
||||
retainWhere(effects, effect => {
|
||||
if (effect.kind !== 'CaptureEffect') {
|
||||
return true;
|
||||
}
|
||||
const places: Set<Place> = new Set();
|
||||
for (const place of effect.places) {
|
||||
const kind = state.kind(place);
|
||||
if (
|
||||
kind.kind === ValueKind.Context ||
|
||||
kind.kind === ValueKind.MaybeFrozen ||
|
||||
kind.kind === ValueKind.Mutable
|
||||
) {
|
||||
places.add(place);
|
||||
}
|
||||
}
|
||||
if (places.size === 0) {
|
||||
return false;
|
||||
}
|
||||
effect.places = places;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
/*
|
||||
* If a closure did not capture any mutable values, then we can consider it to be
|
||||
* frozen, which allows it to be independently memoized.
|
||||
@@ -1280,20 +1293,18 @@ function inferBlock(
|
||||
break;
|
||||
}
|
||||
case 'PropertyStore': {
|
||||
const effect =
|
||||
state.kind(instrValue.object).kind === ValueKind.Context
|
||||
? Effect.ConditionallyMutate
|
||||
: Effect.Capture;
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
instrValue.value,
|
||||
effect,
|
||||
Effect.Capture,
|
||||
ValueReason.Other,
|
||||
);
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
instrValue.object,
|
||||
Effect.Store,
|
||||
isObjectType(instrValue.object.identifier)
|
||||
? Effect.Store
|
||||
: Effect.Mutate,
|
||||
ValueReason.Other,
|
||||
);
|
||||
|
||||
@@ -1320,25 +1331,21 @@ function inferBlock(
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
instrValue.object,
|
||||
Effect.Read,
|
||||
Effect.Capture,
|
||||
ValueReason.Other,
|
||||
);
|
||||
const lvalue = instr.lvalue;
|
||||
lvalue.effect = Effect.ConditionallyMutate;
|
||||
lvalue.effect = Effect.Store;
|
||||
state.initialize(instrValue, state.kind(instrValue.object));
|
||||
state.define(lvalue, instrValue);
|
||||
continuation = {kind: 'funeffects'};
|
||||
break;
|
||||
}
|
||||
case 'ComputedStore': {
|
||||
const effect =
|
||||
state.kind(instrValue.object).kind === ValueKind.Context
|
||||
? Effect.ConditionallyMutate
|
||||
: Effect.Capture;
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
instrValue.value,
|
||||
effect,
|
||||
Effect.Capture,
|
||||
ValueReason.Other,
|
||||
);
|
||||
state.referenceAndRecordEffects(
|
||||
@@ -1350,7 +1357,9 @@ function inferBlock(
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
instrValue.object,
|
||||
Effect.Store,
|
||||
isObjectType(instrValue.object.identifier)
|
||||
? Effect.Store
|
||||
: Effect.Mutate,
|
||||
ValueReason.Other,
|
||||
);
|
||||
|
||||
@@ -1387,7 +1396,7 @@ function inferBlock(
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
instrValue.object,
|
||||
Effect.Read,
|
||||
Effect.Capture,
|
||||
ValueReason.Other,
|
||||
);
|
||||
state.referenceAndRecordEffects(
|
||||
@@ -1397,7 +1406,7 @@ function inferBlock(
|
||||
ValueReason.Other,
|
||||
);
|
||||
const lvalue = instr.lvalue;
|
||||
lvalue.effect = Effect.ConditionallyMutate;
|
||||
lvalue.effect = Effect.Store;
|
||||
state.initialize(instrValue, state.kind(instrValue.object));
|
||||
state.define(lvalue, instrValue);
|
||||
continuation = {kind: 'funeffects'};
|
||||
@@ -1811,7 +1820,9 @@ function inferBlock(
|
||||
state.isDefined(operand) &&
|
||||
((operand.identifier.type.kind === 'Function' &&
|
||||
state.isFunctionExpression) ||
|
||||
state.kind(operand).kind === ValueKind.Context)
|
||||
state.kind(operand).kind === ValueKind.Context ||
|
||||
(state.kind(operand).kind === ValueKind.Mutable &&
|
||||
state.isFunctionExpression))
|
||||
) {
|
||||
/**
|
||||
* Returned values should only be typed as 'frozen' if they are both (1)
|
||||
@@ -1838,22 +1849,6 @@ function inferBlock(
|
||||
);
|
||||
}
|
||||
|
||||
function getContextRefOperand(
|
||||
state: InferenceState,
|
||||
instrValue: InstructionValue,
|
||||
): Array<Place> {
|
||||
const result = [];
|
||||
for (const place of eachInstructionValueOperand(instrValue)) {
|
||||
if (
|
||||
state.isDefined(place) &&
|
||||
state.kind(place).kind === ValueKind.Context
|
||||
) {
|
||||
result.push(place);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getFunctionCallSignature(
|
||||
env: Environment,
|
||||
type: Type,
|
||||
|
||||
@@ -235,6 +235,7 @@ function rewriteBlock(
|
||||
type: null,
|
||||
loc: terminal.loc,
|
||||
},
|
||||
effects: null,
|
||||
});
|
||||
block.terminal = {
|
||||
kind: 'goto',
|
||||
@@ -263,5 +264,6 @@ function declareTemporary(
|
||||
type: null,
|
||||
loc: result.loc,
|
||||
},
|
||||
effects: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
Place,
|
||||
promoteTemporary,
|
||||
SpreadPattern,
|
||||
todoPopulateAliasingEffects,
|
||||
} from '../HIR';
|
||||
import {
|
||||
createTemporaryPlace,
|
||||
@@ -151,6 +152,7 @@ export function inlineJsxTransform(
|
||||
type: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
currentBlockInstructions.push(varInstruction);
|
||||
@@ -167,6 +169,7 @@ export function inlineJsxTransform(
|
||||
},
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
currentBlockInstructions.push(devGlobalInstruction);
|
||||
@@ -220,6 +223,7 @@ export function inlineJsxTransform(
|
||||
type: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
thenBlockInstructions.push(reassignElseInstruction);
|
||||
@@ -292,6 +296,7 @@ export function inlineJsxTransform(
|
||||
],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
elseBlockInstructions.push(reactElementInstruction);
|
||||
@@ -309,6 +314,7 @@ export function inlineJsxTransform(
|
||||
type: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
elseBlockInstructions.push(reassignConditionalInstruction);
|
||||
@@ -436,6 +442,7 @@ function createSymbolProperty(
|
||||
binding: {kind: 'Global', name: 'Symbol'},
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolInstruction);
|
||||
@@ -450,6 +457,7 @@ function createSymbolProperty(
|
||||
property: makePropertyLiteral('for'),
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolForInstruction);
|
||||
@@ -463,6 +471,7 @@ function createSymbolProperty(
|
||||
value: symbolName,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolValueInstruction);
|
||||
@@ -478,6 +487,7 @@ function createSymbolProperty(
|
||||
args: [symbolValueInstruction.lvalue],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
const $$typeofProperty: ObjectProperty = {
|
||||
@@ -508,6 +518,7 @@ function createTagProperty(
|
||||
value: componentTag.name,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
tagProperty = {
|
||||
@@ -634,6 +645,7 @@ function createPropsProperties(
|
||||
elements: [...children],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(childrenPropInstruction);
|
||||
@@ -657,6 +669,7 @@ function createPropsProperties(
|
||||
value: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
refProperty = {
|
||||
@@ -678,6 +691,7 @@ function createPropsProperties(
|
||||
value: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
keyProperty = {
|
||||
@@ -711,6 +725,7 @@ function createPropsProperties(
|
||||
properties: props,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
propsProperty = {
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
markInstructionIds,
|
||||
promoteTemporary,
|
||||
reversePostorderBlocks,
|
||||
todoPopulateAliasingEffects,
|
||||
} from '../HIR';
|
||||
import {createTemporaryPlace} from '../HIR/HIRBuilder';
|
||||
import {enterSSA} from '../SSA';
|
||||
@@ -146,6 +147,7 @@ function emitLoadLoweredContextCallee(
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: createTemporaryPlace(env, GeneratedSource),
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
value: loadGlobal,
|
||||
};
|
||||
}
|
||||
@@ -192,6 +194,7 @@ function emitPropertyLoad(
|
||||
lvalue: object,
|
||||
value: loadObj,
|
||||
id: makeInstructionId(0),
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
|
||||
@@ -206,6 +209,7 @@ function emitPropertyLoad(
|
||||
lvalue: element,
|
||||
value: loadProp,
|
||||
id: makeInstructionId(0),
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return {
|
||||
@@ -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: todoPopulateAliasingEffects(),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return fnInstr;
|
||||
@@ -294,6 +300,7 @@ function emitArrayInstr(elements: Array<Place>, env: Environment): Instruction {
|
||||
id: makeInstructionId(0),
|
||||
value: array,
|
||||
lvalue: arrayLvalue,
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return arrayInstr;
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
Place,
|
||||
promoteTemporary,
|
||||
promoteTemporaryJsxTag,
|
||||
todoPopulateAliasingEffects,
|
||||
} from '../HIR/HIR';
|
||||
import {createTemporaryPlace} from '../HIR/HIRBuilder';
|
||||
import {printIdentifier} from '../HIR/PrintHIR';
|
||||
@@ -297,6 +298,7 @@ function emitOutlinedJsx(
|
||||
},
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
effects: null,
|
||||
};
|
||||
promoteTemporaryJsxTag(loadJsx.lvalue.identifier);
|
||||
const jsxExpr: Instruction = {
|
||||
@@ -312,6 +314,7 @@ function emitOutlinedJsx(
|
||||
openingLoc: GeneratedSource,
|
||||
closingLoc: GeneratedSource,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
};
|
||||
|
||||
return [loadJsx, jsxExpr];
|
||||
@@ -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: todoPopulateAliasingEffects(),
|
||||
};
|
||||
return destructurePropsInstr;
|
||||
}
|
||||
|
||||
@@ -1183,7 +1183,7 @@ function codegenTerminal(
|
||||
? codegenPlaceToExpression(cx, case_.test)
|
||||
: null;
|
||||
const block = codegenBlock(cx, case_.block!);
|
||||
return t.switchCase(test, [block]);
|
||||
return t.switchCase(test, block.body.length === 0 ? [] : [block]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
NonLocalImportSpecifier,
|
||||
Place,
|
||||
promoteTemporary,
|
||||
todoPopulateAliasingEffects,
|
||||
} from '../HIR';
|
||||
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
@@ -436,6 +437,7 @@ function makeLoadUseFireInstruction(
|
||||
value: instrValue,
|
||||
lvalue: {...useFirePlace},
|
||||
loc: GeneratedSource,
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -460,6 +462,7 @@ function makeLoadFireCalleeInstruction(
|
||||
},
|
||||
lvalue: {...loadedFireCallee},
|
||||
loc: GeneratedSource,
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -483,6 +486,7 @@ function makeCallUseFireInstruction(
|
||||
value: useFireCall,
|
||||
lvalue: {...useFireCallResultPlace},
|
||||
loc: GeneratedSource,
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -511,6 +515,7 @@ function makeStoreUseFireInstruction(
|
||||
},
|
||||
lvalue: fireFunctionBindingLValuePlace,
|
||||
loc: GeneratedSource,
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -133,6 +148,19 @@ export function Iterable_some<T>(
|
||||
return false;
|
||||
}
|
||||
|
||||
export function Iterable_filter<T>(
|
||||
iter: Iterable<T>,
|
||||
pred: (item: T) => boolean,
|
||||
): Array<T> {
|
||||
const result: Array<T> = [];
|
||||
for (const item of iter) {
|
||||
if (pred(item)) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function nonNull<T extends NonNullable<U>, U>(
|
||||
value: T | null | undefined,
|
||||
): value is T {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
function useFoo({a, b}: {a: number, b: number}) {
|
||||
const x = [];
|
||||
const y = {value: a};
|
||||
|
||||
arrayPush(x, y); // x and y co-mutate
|
||||
const y_alias = y;
|
||||
const cb = () => y_alias.value;
|
||||
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2, b: 10}],
|
||||
sequentialRenders: [
|
||||
{a: 2, b: 10},
|
||||
{a: 2, b: 11},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { arrayPush, setPropertyByKey, Stringify } from "shared-runtime";
|
||||
|
||||
function useFoo(t0) {
|
||||
const $ = _c(3);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const x = [];
|
||||
const y = { value: a };
|
||||
|
||||
arrayPush(x, y);
|
||||
const y_alias = y;
|
||||
const cb = () => y_alias.value;
|
||||
setPropertyByKey(x[0], "value", b);
|
||||
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{ a: 2, b: 10 }],
|
||||
sequentialRenders: [
|
||||
{ a: 2, b: 10 },
|
||||
{ a: 2, b: 11 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
<div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
function useFoo({a, b}: {a: number, b: number}) {
|
||||
const x = [];
|
||||
const y = {value: a};
|
||||
|
||||
arrayPush(x, y); // x and y co-mutate
|
||||
const y_alias = y;
|
||||
const cb = () => y_alias.value;
|
||||
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2, b: 10}],
|
||||
sequentialRenders: [
|
||||
{a: 2, b: 10},
|
||||
{a: 2, b: 11},
|
||||
],
|
||||
};
|
||||
@@ -5,19 +5,6 @@
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
import {setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Variation of bug in `bug-aliased-capture-aliased-mutate`
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
*/
|
||||
|
||||
function useFoo({a}: {a: number, b: number}) {
|
||||
const arr = [];
|
||||
const obj = {value: a};
|
||||
@@ -46,7 +33,7 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import { setPropertyByKey, Stringify } from "shared-runtime";
|
||||
|
||||
function useFoo(t0) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(2);
|
||||
const { a } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a) {
|
||||
@@ -55,15 +42,7 @@ function useFoo(t0) {
|
||||
|
||||
setPropertyByKey(obj, "arr", arr);
|
||||
const obj_alias = obj;
|
||||
let t2;
|
||||
if ($[2] !== obj_alias.arr.length) {
|
||||
t2 = () => obj_alias.arr.length;
|
||||
$[2] = obj_alias.arr.length;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const cb = t2;
|
||||
const cb = () => obj_alias.arr.length;
|
||||
for (let i = 0; i < a; i++) {
|
||||
arr.push(i);
|
||||
}
|
||||
@@ -84,4 +63,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
<div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
|
||||
@@ -1,19 +1,6 @@
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
import {setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Variation of bug in `bug-aliased-capture-aliased-mutate`
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
*/
|
||||
|
||||
function useFoo({a}: {a: number, b: number}) {
|
||||
const arr = [];
|
||||
const obj = {value: a};
|
||||
@@ -23,34 +23,18 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = _c(6);
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.a) {
|
||||
t0 = { a: props.a };
|
||||
const item = { a: props.a };
|
||||
const items = [item];
|
||||
t0 = items.map(_temp);
|
||||
$[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;
|
||||
const mapped = t0;
|
||||
return mapped;
|
||||
}
|
||||
function _temp(item_0) {
|
||||
|
||||
@@ -50,8 +50,7 @@ function Component(props) {
|
||||
console.log(handlers.value);
|
||||
break bb0;
|
||||
}
|
||||
default: {
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
t0 = handlers;
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
|
||||
* aliases from `LoadLocal`, `PropertyLoad`, etc
|
||||
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
|
||||
* - We avoid assigning mutable ranges to loads after y's mutable range, as
|
||||
* these are working with an immutable value. As a result, `LoadLocal y` and
|
||||
* `PropertyLoad y` do not get mutable ranges
|
||||
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
|
||||
* as according to the 'co-mutation' of different values
|
||||
* - Here, we infer that
|
||||
* - `arrayPush(y, x)` might alias `x` and `y` to each other
|
||||
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
|
||||
* - This pass correctly extends the mutable range of `y`
|
||||
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
|
||||
* PropertyLoads still don't have a mutable range
|
||||
*
|
||||
* Note that the this bug is an edge case. Compiler output is only invalid for:
|
||||
* - function expressions with
|
||||
* `enableTransitivelyFreezeFunctionExpressions:false`
|
||||
* - functions that throw and get retried without clearing the memocache
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
*/
|
||||
function useFoo({a, b}: {a: number, b: number}) {
|
||||
const x = [];
|
||||
const y = {value: a};
|
||||
|
||||
arrayPush(x, y); // x and y co-mutate
|
||||
const y_alias = y;
|
||||
const cb = () => y_alias.value;
|
||||
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2, b: 10}],
|
||||
sequentialRenders: [
|
||||
{a: 2, b: 10},
|
||||
{a: 2, b: 11},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { arrayPush, setPropertyByKey, Stringify } from "shared-runtime";
|
||||
|
||||
function useFoo(t0) {
|
||||
const $ = _c(5);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const x = [];
|
||||
const y = { value: a };
|
||||
|
||||
arrayPush(x, y);
|
||||
const y_alias = y;
|
||||
let t2;
|
||||
if ($[3] !== y_alias.value) {
|
||||
t2 = () => y_alias.value;
|
||||
$[3] = y_alias.value;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
const cb = t2;
|
||||
setPropertyByKey(x[0], "value", b);
|
||||
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{ a: 2, b: 10 }],
|
||||
sequentialRenders: [
|
||||
{ a: 2, b: 10 },
|
||||
{ a: 2, b: 11 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
|
||||
* aliases from `LoadLocal`, `PropertyLoad`, etc
|
||||
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
|
||||
* - We avoid assigning mutable ranges to loads after y's mutable range, as
|
||||
* these are working with an immutable value. As a result, `LoadLocal y` and
|
||||
* `PropertyLoad y` do not get mutable ranges
|
||||
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
|
||||
* as according to the 'co-mutation' of different values
|
||||
* - Here, we infer that
|
||||
* - `arrayPush(y, x)` might alias `x` and `y` to each other
|
||||
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
|
||||
* - This pass correctly extends the mutable range of `y`
|
||||
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
|
||||
* PropertyLoads still don't have a mutable range
|
||||
*
|
||||
* Note that the this bug is an edge case. Compiler output is only invalid for:
|
||||
* - function expressions with
|
||||
* `enableTransitivelyFreezeFunctionExpressions:false`
|
||||
* - functions that throw and get retried without clearing the memocache
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
*/
|
||||
function useFoo({a, b}: {a: number, b: number}) {
|
||||
const x = [];
|
||||
const y = {value: a};
|
||||
|
||||
arrayPush(x, y); // x and y co-mutate
|
||||
const y_alias = y;
|
||||
const cb = () => y_alias.value;
|
||||
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2, b: 10}],
|
||||
sequentialRenders: [
|
||||
{a: 2, b: 10},
|
||||
{a: 2, b: 11},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,171 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
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";
|
||||
import { ValidateMemoization } from "shared-runtime";
|
||||
|
||||
const Codes = {
|
||||
en: { name: "English" },
|
||||
ja: { name: "Japanese" },
|
||||
ko: { name: "Korean" },
|
||||
zh: { name: "Chinese" },
|
||||
};
|
||||
|
||||
function Component(a) {
|
||||
const $ = _c(13);
|
||||
let keys;
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== a) {
|
||||
t1 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
if (a) {
|
||||
keys = Object.keys(Codes);
|
||||
} else {
|
||||
t1 = null;
|
||||
break bb0;
|
||||
}
|
||||
|
||||
t0 = keys.map(_temp);
|
||||
}
|
||||
$[0] = a;
|
||||
$[1] = t0;
|
||||
$[2] = t1;
|
||||
$[3] = keys;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
t1 = $[2];
|
||||
keys = $[3];
|
||||
}
|
||||
if (t1 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t1;
|
||||
}
|
||||
const options = t0;
|
||||
let t2;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = [];
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== keys) {
|
||||
t3 = (
|
||||
<ValidateMemoization inputs={t2} output={keys} onlyCheckCompiled={true} />
|
||||
);
|
||||
$[5] = keys;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
let t4;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = [];
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] !== options) {
|
||||
t5 = (
|
||||
<ValidateMemoization
|
||||
inputs={t4}
|
||||
output={options}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
);
|
||||
$[8] = options;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
let t6;
|
||||
if ($[10] !== t3 || $[11] !== t5) {
|
||||
t6 = (
|
||||
<>
|
||||
{t3}
|
||||
{t5}
|
||||
</>
|
||||
);
|
||||
$[10] = t3;
|
||||
$[11] = t5;
|
||||
$[12] = t6;
|
||||
} else {
|
||||
t6 = $[12];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
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,47 @@
|
||||
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},
|
||||
],
|
||||
};
|
||||
@@ -67,8 +67,7 @@ function Component(props) {
|
||||
case "b": {
|
||||
break bb1;
|
||||
}
|
||||
case "c": {
|
||||
}
|
||||
case "c":
|
||||
default: {
|
||||
x = 6;
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
```
|
||||
|
||||
@@ -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,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,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,83 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component({a, b, c}) {
|
||||
// This is an object version of array-access-assignment.js
|
||||
// Meant to confirm that object expressions and PropertyStore/PropertyLoad with strings
|
||||
// works equivalently to array expressions and property accesses with numeric indices
|
||||
const x = {zero: a};
|
||||
const y = {zero: null, one: b};
|
||||
const z = {zero: {}, one: {}, two: {zero: c}};
|
||||
x.zero = y.one;
|
||||
z.zero.zero = x.zero;
|
||||
return {zero: x, one: z};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 1, b: 20, c: 300}],
|
||||
sequentialRenders: [
|
||||
{a: 2, b: 20, c: 300},
|
||||
{a: 3, b: 20, c: 300},
|
||||
{a: 3, b: 21, c: 300},
|
||||
{a: 3, b: 22, c: 300},
|
||||
{a: 3, b: 22, c: 301},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { a, b, c } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a || $[1] !== b || $[2] !== c) {
|
||||
const x = { zero: a };
|
||||
let t2;
|
||||
if ($[4] !== b) {
|
||||
t2 = { zero: null, one: b };
|
||||
$[4] = b;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const y = t2;
|
||||
const z = { zero: {}, one: {}, two: { zero: c } };
|
||||
x.zero = y.one;
|
||||
z.zero.zero = x.zero;
|
||||
t1 = { zero: x, one: z };
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = c;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 1, b: 20, c: 300 }],
|
||||
sequentialRenders: [
|
||||
{ a: 2, b: 20, c: 300 },
|
||||
{ a: 3, b: 20, c: 300 },
|
||||
{ a: 3, b: 21, c: 300 },
|
||||
{ a: 3, b: 22, c: 300 },
|
||||
{ a: 3, b: 22, c: 301 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) {"zero":{"zero":20},"one":{"zero":{"zero":20},"one":{},"two":{"zero":300}}}
|
||||
{"zero":{"zero":20},"one":{"zero":{"zero":20},"one":{},"two":{"zero":300}}}
|
||||
{"zero":{"zero":21},"one":{"zero":{"zero":21},"one":{},"two":{"zero":300}}}
|
||||
{"zero":{"zero":22},"one":{"zero":{"zero":22},"one":{},"two":{"zero":300}}}
|
||||
{"zero":{"zero":22},"one":{"zero":{"zero":22},"one":{},"two":{"zero":301}}}
|
||||
@@ -0,0 +1,23 @@
|
||||
function Component({a, b, c}) {
|
||||
// This is an object version of array-access-assignment.js
|
||||
// Meant to confirm that object expressions and PropertyStore/PropertyLoad with strings
|
||||
// works equivalently to array expressions and property accesses with numeric indices
|
||||
const x = {zero: a};
|
||||
const y = {zero: null, one: b};
|
||||
const z = {zero: {}, one: {}, two: {zero: c}};
|
||||
x.zero = y.one;
|
||||
z.zero.zero = x.zero;
|
||||
return {zero: x, one: z};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 1, b: 20, c: 300}],
|
||||
sequentialRenders: [
|
||||
{a: 2, b: 20, c: 300},
|
||||
{a: 3, b: 20, c: 300},
|
||||
{a: 3, b: 21, c: 300},
|
||||
{a: 3, b: 22, c: 300},
|
||||
{a: 3, b: 22, c: 301},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoFreezingKnownMutableFunctions
|
||||
|
||||
import {useCallback, useEffect, useRef} from 'react';
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const params = useHook();
|
||||
const update = useCallback(
|
||||
partialParams => {
|
||||
const nextParams = {
|
||||
...params,
|
||||
...partialParams,
|
||||
};
|
||||
// Due to how we previously represented ObjectExpressions in InferReferenceEffects,
|
||||
// this was recorded as a mutation of a context value (`params`) which then made
|
||||
// the function appear ineligible for freezing when passing to useEffect below.
|
||||
nextParams.param = 'value';
|
||||
console.log(nextParams);
|
||||
},
|
||||
[params]
|
||||
);
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
if (ref.current === null) {
|
||||
update();
|
||||
}
|
||||
}, [update]);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoFreezingKnownMutableFunctions
|
||||
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useHook } from "shared-runtime";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(5);
|
||||
const params = useHook();
|
||||
let t0;
|
||||
if ($[0] !== params) {
|
||||
t0 = (partialParams) => {
|
||||
const nextParams = { ...params, ...partialParams };
|
||||
|
||||
nextParams.param = "value";
|
||||
console.log(nextParams);
|
||||
};
|
||||
$[0] = params;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const update = t0;
|
||||
|
||||
const ref = useRef(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[2] !== update) {
|
||||
t1 = () => {
|
||||
if (ref.current === null) {
|
||||
update();
|
||||
}
|
||||
};
|
||||
|
||||
t2 = [update];
|
||||
$[2] = update;
|
||||
$[3] = t1;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t2 = $[4];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
return "ok";
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,30 @@
|
||||
// @validateNoFreezingKnownMutableFunctions
|
||||
|
||||
import {useCallback, useEffect, useRef} from 'react';
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const params = useHook();
|
||||
const update = useCallback(
|
||||
partialParams => {
|
||||
const nextParams = {
|
||||
...params,
|
||||
...partialParams,
|
||||
};
|
||||
// Due to how we previously represented ObjectExpressions in InferReferenceEffects,
|
||||
// this was recorded as a mutation of a context value (`params`) which then made
|
||||
// the function appear ineligible for freezing when passing to useEffect below.
|
||||
nextParams.param = 'value';
|
||||
console.log(nextParams);
|
||||
},
|
||||
[params]
|
||||
);
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
if (ref.current === null) {
|
||||
update();
|
||||
}
|
||||
}, [update]);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
@@ -50,10 +50,8 @@ function Component(props) {
|
||||
case 1: {
|
||||
break bb0;
|
||||
}
|
||||
case 2: {
|
||||
}
|
||||
default: {
|
||||
}
|
||||
case 2:
|
||||
default:
|
||||
}
|
||||
} else {
|
||||
if (props.cond2) {
|
||||
|
||||
@@ -41,8 +41,7 @@ function foo() {
|
||||
case 2: {
|
||||
break bb0;
|
||||
}
|
||||
default: {
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,22 +43,17 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
function foo(x) {
|
||||
bb0: switch (x) {
|
||||
case 0: {
|
||||
}
|
||||
case 1: {
|
||||
}
|
||||
case 0:
|
||||
case 1:
|
||||
case 2: {
|
||||
break bb0;
|
||||
}
|
||||
case 3: {
|
||||
break bb0;
|
||||
}
|
||||
case 4: {
|
||||
}
|
||||
case 5: {
|
||||
}
|
||||
default: {
|
||||
}
|
||||
case 4:
|
||||
case 5:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -453,8 +453,6 @@ const skipFilter = new Set([
|
||||
'inner-function/nullable-objects/bug-invalid-array-map-manual',
|
||||
'bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr',
|
||||
`bug-capturing-func-maybealias-captured-mutate`,
|
||||
'bug-aliased-capture-aliased-mutate',
|
||||
'bug-aliased-capture-mutate',
|
||||
'bug-functiondecl-hoisting',
|
||||
'bug-type-inference-control-flow',
|
||||
'fbt/bug-fbt-plural-multiple-function-calls',
|
||||
@@ -485,6 +483,7 @@ const skipFilter = new Set([
|
||||
'todo.lower-context-access-array-destructuring',
|
||||
'lower-context-selector-simple',
|
||||
'lower-context-acess-multiple',
|
||||
'bug-separate-memoization-due-to-callback-capturing',
|
||||
]);
|
||||
|
||||
export default skipFilter;
|
||||
|
||||
@@ -42,6 +42,7 @@ export function runSprout(
|
||||
(globalThis as any).__SNAP_EVALUATOR_MODE = undefined;
|
||||
}
|
||||
if (forgetResult.kind === 'UnexpectedError') {
|
||||
console.log(forgetCode);
|
||||
return makeError('Unexpected error in Forget runner', forgetResult.value);
|
||||
}
|
||||
if (originalCode.indexOf('@disableNonForgetInSprout') === -1) {
|
||||
|
||||
Reference in New Issue
Block a user