Compare commits
34 Commits
pr35832
...
gh/josephs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcf9bc817c | ||
|
|
c3b87cc620 | ||
|
|
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
|
||||
|
||||
|
||||
@@ -225,6 +225,7 @@ export function lower(
|
||||
loc: func.node.loc ?? GeneratedSource,
|
||||
env,
|
||||
effects: null,
|
||||
aliasingEffects: null,
|
||||
directives,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -280,6 +280,7 @@ export type HIRFunction = {
|
||||
returnType: Type;
|
||||
context: Array<Place>;
|
||||
effects: Array<FunctionEffect> | null;
|
||||
aliasingEffects: Array<AliasingEffect> | null;
|
||||
body: HIR;
|
||||
generator: boolean;
|
||||
async: boolean;
|
||||
@@ -300,6 +301,10 @@ export type FunctionEffect =
|
||||
places: ReadonlySet<Place>;
|
||||
effect: Effect;
|
||||
loc: SourceLocation;
|
||||
}
|
||||
| {
|
||||
kind: 'CaptureEffect';
|
||||
places: ReadonlySet<Place>;
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -1430,6 +1435,46 @@ export const ValueKindSchema = z.enum([
|
||||
ValueKind.Context,
|
||||
]);
|
||||
|
||||
export type AliasingEffect =
|
||||
/**
|
||||
* Freezes the operand if not already frozen
|
||||
*/
|
||||
| {kind: 'Freeze'; place: Place}
|
||||
/**
|
||||
* Known mutation of a value, which may effect that value or values that it contains.
|
||||
* This is rare!
|
||||
*/
|
||||
| {kind: 'MutateTransitive'; place: Place}
|
||||
/**
|
||||
* Known mutation of a specific value, targeting only that specific value but not
|
||||
* values that it contains.
|
||||
*
|
||||
* Example: `array.push(item)` mutates the array but does not mutate items stored in the array.
|
||||
*/
|
||||
| {kind: 'MutateLocal'; place: Place}
|
||||
/**
|
||||
* Possible mutation of a specific value
|
||||
*/
|
||||
| {kind: 'ConditionallyMutate'; place: Place}
|
||||
/**
|
||||
* Direct aliasing of one identifier by another identifier
|
||||
* Examples: `x = y` (from y -> to x) or phis (from operand -> to phi)
|
||||
*/
|
||||
| {kind: 'Alias'; from: Place; to: Place}
|
||||
/**
|
||||
* Direct aliasing of an identifier (or a sub-path), or storing a value/sub-path
|
||||
* into a part of another object.
|
||||
*
|
||||
* One of from.path and/or to.path must be non-null (else this is equivalent to Alias)
|
||||
*/
|
||||
| {
|
||||
kind: 'Capture';
|
||||
from: {place: Place; path: '*' | null};
|
||||
to: {place: Place; path: '*' | null};
|
||||
}
|
||||
// Known mutation of a global
|
||||
| {kind: 'MutateGlobal'; place: Place};
|
||||
|
||||
// The effect with which a value is modified.
|
||||
export enum Effect {
|
||||
// Default value: not allowed after lifetime inference
|
||||
|
||||
@@ -179,8 +179,59 @@ export type FunctionSignature = {
|
||||
impure?: boolean;
|
||||
|
||||
canonicalName?: string;
|
||||
|
||||
aliasing?: AliasingSignature;
|
||||
};
|
||||
|
||||
export type LifetimeId = number;
|
||||
export type AliasingSignature = {
|
||||
receiver: LifetimeId;
|
||||
params: Array<LifetimeId> | null;
|
||||
restParam: LifetimeId;
|
||||
returns: LifetimeId;
|
||||
effects: Array<AliasingSignatureEffect>;
|
||||
};
|
||||
|
||||
export type AliasingSignatureEffect =
|
||||
/**
|
||||
* Freezes the operand if not already frozen
|
||||
*/
|
||||
| {kind: 'Freeze'; place: LifetimeId}
|
||||
/**
|
||||
* Known mutation of a value, which may effect that value or values that it contains.
|
||||
* This is rare!
|
||||
*/
|
||||
| {kind: 'MutateTransitive'; place: LifetimeId}
|
||||
/**
|
||||
* Known mutation of a specific value, targeting only that specific value but not
|
||||
* values that it contains.
|
||||
*
|
||||
* Example: `array.push(item)` mutates the array but does not mutate items stored in the array.
|
||||
*/
|
||||
| {kind: 'MutateLocal'; place: LifetimeId}
|
||||
/**
|
||||
* Possible mutation of a specific value
|
||||
*/
|
||||
| {kind: 'ConditionallyMutate'; place: LifetimeId}
|
||||
/**
|
||||
* Direct aliasing of one identifier by another identifier
|
||||
* Examples: `x = y` (from y -> to x) or phis (from operand -> to phi)
|
||||
*/
|
||||
| {kind: 'Alias'; from: LifetimeId; to: LifetimeId}
|
||||
/**
|
||||
* Direct aliasing of an identifier (or a sub-path), or storing a value/sub-path
|
||||
* into a part of another object.
|
||||
*
|
||||
* One of from.path and/or to.path must be non-null (else this is equivalent to Alias)
|
||||
*/
|
||||
| {
|
||||
kind: 'Capture';
|
||||
from: {place: LifetimeId; path: '*' | null};
|
||||
to: {place: LifetimeId; path: '*' | null};
|
||||
}
|
||||
// Known mutation of a global
|
||||
| {kind: 'MutateGlobal'; place: LifetimeId};
|
||||
|
||||
/*
|
||||
* Shape of an {@link FunctionType} if {@link ObjectShape.functionType} is present,
|
||||
* or {@link ObjectType} otherwise.
|
||||
|
||||
@@ -546,12 +546,23 @@ 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(', ') ?? '';
|
||||
@@ -720,7 +731,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;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
LoweredFunction,
|
||||
Place,
|
||||
isRefOrRefValue,
|
||||
makeInstructionId,
|
||||
} from '../HIR';
|
||||
@@ -19,6 +20,12 @@ 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 {Iterable_some} from '../Utils/utils';
|
||||
|
||||
export default function analyseFunctions(func: HIRFunction): void {
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
@@ -26,8 +33,8 @@ export default function analyseFunctions(func: HIRFunction): void {
|
||||
switch (instr.value.kind) {
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
lower(instr.value.loweredFunc.func);
|
||||
infer(instr.value.loweredFunc);
|
||||
const aliases = lower(instr.value.loweredFunc.func);
|
||||
infer(instr.value.loweredFunc, aliases);
|
||||
|
||||
/**
|
||||
* Reset mutable range for outer inferReferenceEffects
|
||||
@@ -44,11 +51,11 @@ export default function analyseFunctions(func: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
|
||||
function lower(func: HIRFunction): void {
|
||||
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 +63,61 @@ function lower(func: HIRFunction): void {
|
||||
name: 'AnalyseFunction (inner)',
|
||||
value: func,
|
||||
});
|
||||
inferAliasesForCapturing(func, aliases);
|
||||
return aliases;
|
||||
}
|
||||
|
||||
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 +144,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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,26 +311,31 @@ 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 {
|
||||
|
||||
@@ -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, printMutableRange} 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,57 @@ 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) + printMutableRange(ident),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,556 @@
|
||||
/**
|
||||
* 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 {isFunctionExpression} from '@babel/types';
|
||||
import {CompilerError} from '..';
|
||||
import {
|
||||
AliasingEffect,
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
CallExpression,
|
||||
Environment,
|
||||
HIRFunction,
|
||||
Instruction,
|
||||
InstructionValue,
|
||||
isArrayType,
|
||||
isMapType,
|
||||
isSetType,
|
||||
MethodCall,
|
||||
NewExpression,
|
||||
Place,
|
||||
SpreadPattern,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionValueLValue,
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalSuccessor,
|
||||
} from '../HIR/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {getFunctionCallSignature} from './InferReferenceEffects';
|
||||
import {
|
||||
AliasingSignature,
|
||||
FunctionSignature,
|
||||
LifetimeId,
|
||||
} from '../HIR/ObjectShape';
|
||||
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
|
||||
|
||||
export function inferMutationAliasingEffects(
|
||||
fn: HIRFunction,
|
||||
{isFunctionExpression}: {isFunctionExpression: boolean} = {
|
||||
isFunctionExpression: false,
|
||||
},
|
||||
): Result<Array<AliasingEffect>, CompilerError> {
|
||||
const initialState = InferenceState.empty(fn.env, isFunctionExpression);
|
||||
|
||||
// Map of blocks to the last (merged) incoming state that was processed
|
||||
const statesByBlock: Map<BlockId, InferenceState> = new Map();
|
||||
|
||||
/*
|
||||
* Multiple predecessors may be visited prior to reaching a given successor,
|
||||
* so track the list of incoming state for each successor block.
|
||||
* These are merged when reaching that block again.
|
||||
*/
|
||||
const queuedStates: Map<BlockId, InferenceState> = new Map();
|
||||
function queue(blockId: BlockId, state: InferenceState): void {
|
||||
let queuedState = queuedStates.get(blockId);
|
||||
if (queuedState != null) {
|
||||
// merge the queued states for this block
|
||||
state = queuedState.merge(state) ?? queuedState;
|
||||
queuedStates.set(blockId, state);
|
||||
} else {
|
||||
/*
|
||||
* this is the first queued state for this block, see whether
|
||||
* there are changed relative to the last time it was processed.
|
||||
*/
|
||||
const prevState = statesByBlock.get(blockId);
|
||||
const nextState = prevState != null ? prevState.merge(state) : state;
|
||||
if (nextState != null) {
|
||||
queuedStates.set(blockId, nextState);
|
||||
}
|
||||
}
|
||||
}
|
||||
queue(fn.body.entry, initialState);
|
||||
|
||||
const signatureCache: Map<Instruction, Array<AliasingEffect>> = new Map();
|
||||
|
||||
while (queuedStates.size !== 0) {
|
||||
for (const [blockId, block] of fn.body.blocks) {
|
||||
const incomingState = queuedStates.get(blockId);
|
||||
queuedStates.delete(blockId);
|
||||
if (incomingState == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
statesByBlock.set(blockId, incomingState);
|
||||
const state = incomingState.clone();
|
||||
inferBlock(state, block, signatureCache);
|
||||
|
||||
for (const nextBlockId of eachTerminalSuccessor(block.terminal)) {
|
||||
queue(nextBlockId, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function inferBlock(
|
||||
state: InferenceState,
|
||||
block: BasicBlock,
|
||||
signatureCache: Map<Instruction, Array<AliasingEffect>>,
|
||||
): void {
|
||||
for (const instr of block.instructions) {
|
||||
let signature = signatureCache.get(instr);
|
||||
if (signature == null) {
|
||||
signature = computeSignatureForInstruction(state.env, instr);
|
||||
signatureCache.set(instr, signature);
|
||||
}
|
||||
const effects = applySignature(signature, state);
|
||||
instr.effects = effects;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the signature to the given state to determine the precise set of effects
|
||||
* that will occur in practice. This takes into account the inferred state of each
|
||||
* variable. For example, the signature may have a `ConditionallyMutate x` effect.
|
||||
* Here, we check the abstract type of `x` and either record a `Mutate x` if x is mutable
|
||||
* or no effect if x is a primitive, global, or frozen.
|
||||
*
|
||||
* This phase may also emit errors, for example MutateLocal on a frozen value is invalid.
|
||||
*/
|
||||
function applySignature(
|
||||
signature: Array<AliasingEffect>,
|
||||
state: InferenceState,
|
||||
): Array<AliasingEffect> {}
|
||||
|
||||
class InferenceState {
|
||||
env: Environment;
|
||||
isFunctionExpression: boolean;
|
||||
|
||||
constructor(env: Environment, isFunctionExpression: boolean) {
|
||||
this.env = env;
|
||||
this.isFunctionExpression = isFunctionExpression;
|
||||
}
|
||||
|
||||
merge(state: InferenceState): InferenceState | null {}
|
||||
|
||||
clone(): InferenceState {}
|
||||
|
||||
static empty(
|
||||
env: Environment,
|
||||
isFunctionExpression: boolean,
|
||||
): InferenceState {
|
||||
return new InferenceState(env, isFunctionExpression);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes an effect signature for the instruction _without_ looking at the inference state,
|
||||
* and only using the semantics of the instructions and the inferred types. The idea is to make
|
||||
* it easy to check that the semantics of each instruction are preserved by describing only the
|
||||
* effects and not making decisions based on the inference state.
|
||||
*
|
||||
* Then in applySignature(), above, we refine this signature based on the inference state.
|
||||
*
|
||||
* NOTE: this function is designed to be cached so it's only computed once upon first visiting
|
||||
* an instruction.
|
||||
*/
|
||||
function computeSignatureForInstruction(
|
||||
env: Environment,
|
||||
instr: Instruction,
|
||||
): Array<AliasingEffect> {
|
||||
const {lvalue, value} = instr;
|
||||
const effects: Array<AliasingEffect> = [];
|
||||
switch (value.kind) {
|
||||
case 'ArrayExpression': {
|
||||
// All elements are captured into part of the output value
|
||||
for (const element of value.elements) {
|
||||
let operand: Place;
|
||||
if (element.kind === 'Identifier') {
|
||||
operand = element;
|
||||
} else if (element.kind === 'Spread') {
|
||||
operand = element.place;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: {place: operand, path: null},
|
||||
to: {place: lvalue, path: '*'},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ObjectExpression': {
|
||||
for (const property of value.properties) {
|
||||
const operand =
|
||||
property.kind === 'ObjectProperty' ? property.place : property.place;
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: {place: operand, path: null},
|
||||
to: {place: lvalue, path: '*'},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Await': {
|
||||
// Potentially mutates the receiver (awaiting it changes its state and can run side effects)
|
||||
effects.push({kind: 'ConditionallyMutate', place: value.value});
|
||||
/**
|
||||
* Data from the promise may be returned into the result, but await does not directly return
|
||||
* the promise itself
|
||||
*/
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: {place: value.value, path: '*'},
|
||||
to: {place: lvalue, path: null},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'TaggedTemplateExpression': {
|
||||
CompilerError.throwTodo({
|
||||
reason: `Handle TaggedTemplateExpression in new inference`,
|
||||
loc: instr.loc,
|
||||
});
|
||||
}
|
||||
case 'NewExpression':
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
let callee;
|
||||
let mutatesCallee = false;
|
||||
if (value.kind === 'NewExpression') {
|
||||
callee = value.callee;
|
||||
mutatesCallee = false;
|
||||
} else if (value.kind === 'CallExpression') {
|
||||
callee = value.callee;
|
||||
mutatesCallee = true;
|
||||
} else if (value.kind === 'MethodCall') {
|
||||
callee = value.property;
|
||||
mutatesCallee = false;
|
||||
} else {
|
||||
assertExhaustive(
|
||||
value,
|
||||
`Unexpected value kind '${(value as any).kind}'`,
|
||||
);
|
||||
}
|
||||
const signature = getFunctionCallSignature(env, callee.identifier.type);
|
||||
if (signature != null && signature.aliasing != null) {
|
||||
effects.push(
|
||||
...computeEffectsForSignature(
|
||||
signature.aliasing,
|
||||
lvalue,
|
||||
callee,
|
||||
value.args,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
/**
|
||||
* If no signature then by default:
|
||||
* - All operands are conditionally mutated, except some instruction
|
||||
* variants are assumed to not mutate the callee (such as `new`)
|
||||
* - All operands are captured into (but not directly aliased as)
|
||||
* every other argument.
|
||||
*/
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (operand !== callee || mutatesCallee) {
|
||||
effects.push({kind: 'ConditionallyMutate', place: operand});
|
||||
}
|
||||
for (const other of eachInstructionValueOperand(value)) {
|
||||
if (other === operand) {
|
||||
continue;
|
||||
}
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: {place: operand, path: null},
|
||||
to: {place: other, path: '*'},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'PropertyDelete':
|
||||
case 'ComputedDelete': {
|
||||
// Mutates the object by removing the property, no aliasing
|
||||
effects.push({kind: 'MutateLocal', place: value.object});
|
||||
break;
|
||||
}
|
||||
case 'PropertyLoad':
|
||||
case 'ComputedLoad': {
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: {place: value.object, path: '*'},
|
||||
to: {place: lvalue, path: null},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'PropertyStore':
|
||||
case 'ComputedStore': {
|
||||
effects.push({kind: 'MutateLocal', place: value.object});
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: {place: value.value, path: null},
|
||||
to: {place: value.object, path: '*'},
|
||||
});
|
||||
effects.push({kind: 'Alias', from: value.value, to: lvalue});
|
||||
break;
|
||||
}
|
||||
case 'PostfixUpdate':
|
||||
case 'PrefixUpdate': {
|
||||
effects.push({kind: 'MutateLocal', place: value.value});
|
||||
break;
|
||||
}
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
const functionEffects = value.loweredFunc.func.aliasingEffects;
|
||||
if (functionEffects != null) {
|
||||
effects.push(...functionEffects);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'GetIterator': {
|
||||
if (
|
||||
isArrayType(value.collection.identifier) ||
|
||||
isMapType(value.collection.identifier) ||
|
||||
isSetType(value.collection.identifier)
|
||||
) {
|
||||
/*
|
||||
* Builtin collections are known to return a fresh iterator on each call,
|
||||
* so the iterator does not alias the collection
|
||||
*/
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: {place: value.collection, path: '*'},
|
||||
to: {place: lvalue, path: '*'},
|
||||
});
|
||||
} else {
|
||||
/*
|
||||
* Otherwise, the object may return itself as the iterator, so we have to
|
||||
* assume that the result directly aliases the collection. Further, the
|
||||
* method to get the iterator could potentially mutate the collection
|
||||
*/
|
||||
effects.push({kind: 'Alias', from: value.collection, to: lvalue});
|
||||
effects.push({kind: 'ConditionallyMutate', place: value.collection});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'IteratorNext': {
|
||||
/*
|
||||
* Technically advancing an iterator will always mutate it (for any reasonable implementation)
|
||||
* But because we create an alias from the collection to the iterator if we don't know the type,
|
||||
* then it's possible the iterator is aliased to a frozen value and we wouldn't want to error.
|
||||
* so we mark this as conditional mutation to allow iterating frozen values.
|
||||
*/
|
||||
effects.push({kind: 'ConditionallyMutate', place: value.iterator});
|
||||
// Extracts part of the original collection into the result
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: {place: value.iterator, path: '*'},
|
||||
to: {place: lvalue, path: null},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'NextPropertyOf': {
|
||||
// no effects
|
||||
break;
|
||||
}
|
||||
case 'JsxExpression':
|
||||
case 'JsxFragment': {
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
effects.push({kind: 'Freeze', place: operand});
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: {place: operand, path: null},
|
||||
to: {place: lvalue, path: '*'},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'DeclareContext':
|
||||
case 'DeclareLocal': {
|
||||
// no effects
|
||||
break;
|
||||
}
|
||||
case 'Destructure': {
|
||||
for (const patternLValue of eachInstructionValueLValue(value)) {
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: {place: value.value, path: '*'},
|
||||
to: {place: patternLValue, path: null},
|
||||
});
|
||||
}
|
||||
effects.push({kind: 'Alias', from: value.value, to: lvalue});
|
||||
break;
|
||||
}
|
||||
case 'LoadContext': {
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: {place: value.place, path: '*'},
|
||||
to: {place: lvalue, path: null},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
effects.push({kind: 'Alias', from: value.place, to: lvalue});
|
||||
break;
|
||||
}
|
||||
case 'StoreContext': {
|
||||
effects.push({kind: 'MutateLocal', place: value.lvalue.place});
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: {place: value.value, path: null},
|
||||
to: {place: value.lvalue.place, path: '*'},
|
||||
});
|
||||
effects.push({kind: 'Alias', from: value.value, to: lvalue});
|
||||
break;
|
||||
}
|
||||
case 'StoreGlobal': {
|
||||
effects.push({kind: 'MutateGlobal', place: value.value});
|
||||
break;
|
||||
}
|
||||
case 'StoreLocal': {
|
||||
effects.push({kind: 'Alias', from: value.value, to: value.lvalue.place});
|
||||
effects.push({kind: 'Alias', from: value.value, to: lvalue});
|
||||
break;
|
||||
}
|
||||
case 'TypeCastExpression': {
|
||||
effects.push({kind: 'Alias', from: value.value, to: lvalue});
|
||||
break;
|
||||
}
|
||||
case 'BinaryExpression':
|
||||
case 'Debugger':
|
||||
case 'FinishMemoize':
|
||||
case 'JSXText':
|
||||
case 'LoadGlobal':
|
||||
case 'MetaProperty':
|
||||
case 'Primitive':
|
||||
case 'RegExpLiteral':
|
||||
case 'StartMemoize':
|
||||
case 'TemplateLiteral':
|
||||
case 'UnaryExpression':
|
||||
case 'UnsupportedNode': {
|
||||
// no effects
|
||||
break;
|
||||
}
|
||||
}
|
||||
return effects;
|
||||
}
|
||||
|
||||
function computeEffectsForSignature(
|
||||
signature: AliasingSignature,
|
||||
lvalue: Place,
|
||||
receiver: Place,
|
||||
args: Array<Place | SpreadPattern>,
|
||||
): Array<AliasingEffect> {
|
||||
// Build substitutions
|
||||
const substitutions: Map<LifetimeId, Array<Place>> = new Map();
|
||||
substitutions.set(signature.receiver, [receiver]);
|
||||
substitutions.set(signature.returns, [lvalue]);
|
||||
const params = signature.params;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (params == null || i >= params.length || arg.kind === 'Spread') {
|
||||
const place = arg.kind === 'Identifier' ? arg : arg.place;
|
||||
getOrInsertWith(substitutions, signature.restParam, () => []).push(place);
|
||||
} else {
|
||||
const param = params[i];
|
||||
substitutions.set(param, [arg]);
|
||||
}
|
||||
}
|
||||
// Apply substitutions
|
||||
const effects: Array<AliasingEffect> = [];
|
||||
for (const effect of signature.effects) {
|
||||
switch (effect.kind) {
|
||||
case 'Alias': {
|
||||
const from = substitutions.get(effect.from);
|
||||
const to = substitutions.get(effect.to);
|
||||
if (from == null || to == null) {
|
||||
continue;
|
||||
}
|
||||
for (const fromPlace of from) {
|
||||
for (const toPlace of to) {
|
||||
effects.push({kind: 'Alias', from: fromPlace, to: toPlace});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Capture': {
|
||||
const from = substitutions.get(effect.from.place);
|
||||
const to = substitutions.get(effect.to.place);
|
||||
if (from == null || to == null) {
|
||||
continue;
|
||||
}
|
||||
for (const fromPlace of from) {
|
||||
for (const toPlace of to) {
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: {place: fromPlace, path: effect.from.path},
|
||||
to: {place: toPlace, path: effect.to.path},
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ConditionallyMutate': {
|
||||
const places = substitutions.get(effect.place);
|
||||
if (places == null) {
|
||||
continue;
|
||||
}
|
||||
for (const place of places) {
|
||||
effects.push({kind: 'ConditionallyMutate', place});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MutateGlobal': {
|
||||
const places = substitutions.get(effect.place);
|
||||
if (places == null) {
|
||||
continue;
|
||||
}
|
||||
for (const place of places) {
|
||||
effects.push({kind: 'MutateGlobal', place});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Freeze': {
|
||||
const places = substitutions.get(effect.place);
|
||||
if (places == null) {
|
||||
continue;
|
||||
}
|
||||
for (const place of places) {
|
||||
effects.push({kind: 'Freeze', place});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MutateLocal': {
|
||||
const places = substitutions.get(effect.place);
|
||||
if (places == null) {
|
||||
continue;
|
||||
}
|
||||
for (const place of places) {
|
||||
effects.push({kind: 'MutateLocal', place});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MutateTransitive': {
|
||||
const places = substitutions.get(effect.place);
|
||||
if (places == null) {
|
||||
continue;
|
||||
}
|
||||
for (const place of places) {
|
||||
effects.push({kind: 'MutateTransitive', place});
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
effect,
|
||||
`Unexpected effect kind '${(effect as any).kind}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return effects;
|
||||
}
|
||||
@@ -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} 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) {
|
||||
@@ -902,19 +913,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 +938,7 @@ function inferBlock(
|
||||
let _: 'Hole' = element.kind;
|
||||
}
|
||||
}
|
||||
|
||||
state.initialize(instrValue, valueKind);
|
||||
state.define(instr.lvalue, instrValue);
|
||||
instr.lvalue.effect = Effect.Store;
|
||||
@@ -954,19 +958,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 +1186,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 +1305,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 +1343,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 +1369,9 @@ function inferBlock(
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
instrValue.object,
|
||||
Effect.Store,
|
||||
isObjectType(instrValue.object.identifier)
|
||||
? Effect.Store
|
||||
: Effect.Mutate,
|
||||
ValueReason.Other,
|
||||
);
|
||||
|
||||
@@ -1387,7 +1408,7 @@ function inferBlock(
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
instrValue.object,
|
||||
Effect.Read,
|
||||
Effect.Capture,
|
||||
ValueReason.Other,
|
||||
);
|
||||
state.referenceAndRecordEffects(
|
||||
@@ -1397,7 +1418,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 +1832,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 +1861,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,
|
||||
|
||||
@@ -252,6 +252,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
returnType: makeType(),
|
||||
context: [],
|
||||
effects: null,
|
||||
aliasingEffects: null,
|
||||
body: {
|
||||
entry: block.id,
|
||||
blocks: new Map([[block.id, block]]),
|
||||
|
||||
@@ -368,6 +368,7 @@ function emitOutlinedFn(
|
||||
returnType: makeType(),
|
||||
context: [],
|
||||
effects: null,
|
||||
aliasingEffects: null,
|
||||
body: {
|
||||
entry: block.id,
|
||||
blocks: new Map([[block.id, block]]),
|
||||
|
||||
@@ -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]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user