Compare commits

...

13 Commits

Author SHA1 Message Date
Mike Vitousek
adf93657ac Update on "[compiler][ez] Simplify continuation structure in InferReferenceEffects"
Test Plan:
Now that function effects aren't computed in InferReferenceEffects, drop the 'funeffects' continuation variant and replace it with just null

[ghstack-poisoned]
2024-09-16 16:26:27 -07:00
Mike Vitousek
26dce8c6c1 Update base for Update on "[compiler][ez] Simplify continuation structure in InferReferenceEffects"
Test Plan:
Now that function effects aren't computed in InferReferenceEffects, drop the 'funeffects' continuation variant and replace it with just null

[ghstack-poisoned]
2024-09-16 16:26:27 -07:00
Mike Vitousek
7347f50ea6 Update on "[compiler][ez] Simplify continuation structure in InferReferenceEffects"
Test Plan:
Now that function effects aren't computed in InferReferenceEffects, drop the 'funeffects' continuation variant and replace it with just null

[ghstack-poisoned]
2024-09-16 15:49:51 -07:00
Mike Vitousek
5013f9bf45 Update base for Update on "[compiler][ez] Simplify continuation structure in InferReferenceEffects"
Test Plan:
Now that function effects aren't computed in InferReferenceEffects, drop the 'funeffects' continuation variant and replace it with just null

[ghstack-poisoned]
2024-09-16 15:49:51 -07:00
Mike Vitousek
6541dd860d Update on "[compiler][ez] Simplify continuation structure in InferReferenceEffects"
Test Plan:
Now that function effects aren't computed in InferReferenceEffects, drop the 'funeffects' continuation variant and replace it with just null

[ghstack-poisoned]
2024-09-16 13:16:20 -07:00
Mike Vitousek
7f9b7dac5c Update base for Update on "[compiler][ez] Simplify continuation structure in InferReferenceEffects"
Test Plan:
Now that function effects aren't computed in InferReferenceEffects, drop the 'funeffects' continuation variant and replace it with just null

[ghstack-poisoned]
2024-09-16 13:16:19 -07:00
Mike Vitousek
82daf0e397 Update on "[compiler][ez] Simplify continuation structure in InferReferenceEffects"
Test Plan:
Now that function effects aren't computed in InferReferenceEffects, drop the 'funeffects' continuation variant and replace it with just null

[ghstack-poisoned]
2024-09-16 11:09:16 -07:00
Mike Vitousek
be0a027cbf Update base for Update on "[compiler][ez] Simplify continuation structure in InferReferenceEffects"
Test Plan:
Now that function effects aren't computed in InferReferenceEffects, drop the 'funeffects' continuation variant and replace it with just null

[ghstack-poisoned]
2024-09-16 11:09:15 -07:00
Mike Vitousek
3154d4fd5f [compiler][ez] Simplify continuation structure in InferReferenceEffects
Test Plan:
Now that function effects aren't computed in InferReferenceEffects, drop the 'funeffects' continuation variant and replace it with just null

[ghstack-poisoned]
2024-09-16 10:55:03 -07:00
Mike Vitousek
ba8fb01ad0 [compiler] Separate InferFunctionEffects pass from InferReferenceEffects
Test Plan:
This diff finally separates InferFunctionEffects into its own separate pass from InferReferenceEffects. It relies on the abstractValues populated in IRE as well as the alias sets that IRE returns.

The meat of the InferFunctionEffects algorithm is still the same, but rather than querying the "live" InferenceState from IRE for abstract values and alias information, we query the values defined on places and the alias set returned by IRE.

One extra bit of work that we now need to perform is creating a map from IdentifierIds to AbstractValues, which we do by traversing the HIRFunction and examining all places. We also need to track the computed effects of nested functions, and that's where the disjoint set of aliases comes in -- when performing the main algorithm, we might see a set of instructions like

```
$0 = Function (effect=ContextMutation) { ... }
$1 = LoadLocal $0
$2 = Call $1 ()
```

When examining the `Call` instruction, we need to know that $1 has the function effect [ContextMutation]. Since $0 and $1 were inferred to be aliased by IRE, we don't need to do any other propagation from $0 to $1 if we track the nested effects based on the root of the alias set { $0, $1 } rather than the specific identifier $1 present in the Call instruction.

[ghstack-poisoned]
2024-09-16 10:54:59 -07:00
Mike Vitousek
10d8b63bac [compiler] InferReferenceEffects outputs a disjoint set of aliases
Test Plan:
For the purposes of a pass added later in this stack, the InferReferenceEffects now outputs a DisjointSet of identifier ids representing aliases. We add this to InferReferenceEffects because this pass already does its own alias analysis, so making it available to later passes is convenient.

This entails implementing copy() and equals() methods on DIsjointSets so that the InferenceState copy and merge methods can handle alias sets.

[ghstack-poisoned]
2024-09-16 10:54:54 -07:00
Mike Vitousek
0836ca3740 [compiler] Initialize abstract values of places in InferReferenceEffects
Test Plan:
In this diff, we now populate the abstractValue field of places during the InferReferenceEffects pass. The value we populate it with is the same value that we use internally in this pass, but it now will remain accessible to downstream phases as part of the Place.

For phis specifically, we need to do a bit of extra work to compute the appropriate value for the phi, since InferReferenceEffects currently doesn't need to infer a value for phis themselves, only values downstream of them. However, the value we compute should correspond to the value available downstream of the phi.

This changes the error message for one todo test case, because we now are querying for the valueKind earlier in the pass, and hitting an invariant violation as a result of that rather than a later invariant violation.

[ghstack-poisoned]
2024-09-16 10:54:49 -07:00
Mike Vitousek
6a77fd2ff7 [compiler] Add nullable abstract value field to places and phis
Test Plan:
This PR starts the process of tracking abstract values (and therefore value kinds) on a per-place basis and persisting that into the place data structure. Here, we simply add a nullable field for abstract values to all places and phis--it will be populated in the next PR.

[ghstack-poisoned]
2024-09-16 10:54:44 -07:00
16 changed files with 433 additions and 153 deletions

View File

@@ -101,6 +101,7 @@ import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetStateInPassiveEffects';
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
import {inferFunctionEffects} from '../Inference/InferFunctionEffects';
import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
export type CompilerPipelineValue =
@@ -210,9 +211,12 @@ function* runWithEnvironment(
analyseFunctions(hir);
yield log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
inferReferenceEffects(hir);
const referenceAliases = inferReferenceEffects(hir);
yield log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
inferFunctionEffects(hir, referenceAliases);
yield log({kind: 'hir', name: 'InferFunctionEffects', value: hir});
validateLocalsNotReassignedAfterRender(hir);
// Note: Has to come after infer reference effects because "dead" code may still affect inference

View File

@@ -82,6 +82,7 @@ export function lower(
kind: 'Identifier',
identifier: builder.resolveBinding(ref),
effect: Effect.Unknown,
abstractValue: null,
reactive: false,
loc: ref.loc ?? GeneratedSource,
});
@@ -113,6 +114,7 @@ export function lower(
kind: 'Identifier',
identifier: binding.identifier,
effect: Effect.Unknown,
abstractValue: null,
reactive: false,
loc: param.node.loc ?? GeneratedSource,
};
@@ -126,6 +128,7 @@ export function lower(
kind: 'Identifier',
identifier: builder.makeTemporary(param.node.loc ?? GeneratedSource),
effect: Effect.Unknown,
abstractValue: null,
reactive: false,
loc: param.node.loc ?? GeneratedSource,
};
@@ -144,6 +147,7 @@ export function lower(
kind: 'Identifier',
identifier: builder.makeTemporary(param.node.loc ?? GeneratedSource),
effect: Effect.Unknown,
abstractValue: null,
reactive: false,
loc: param.node.loc ?? GeneratedSource,
};
@@ -460,6 +464,7 @@ function lowerStatement(
});
const place: Place = {
effect: Effect.Unknown,
abstractValue: null,
identifier: identifier.identifier,
kind: 'Identifier',
reactive: false,
@@ -853,6 +858,7 @@ function lowerStatement(
} else {
const place: Place = {
effect: Effect.Unknown,
abstractValue: null,
identifier: binding.identifier,
kind: 'Identifier',
reactive: false,
@@ -1264,6 +1270,7 @@ function lowerStatement(
handlerBindingPath.node.loc ?? GeneratedSource,
),
effect: Effect.Unknown,
abstractValue: null,
reactive: false,
loc: handlerBindingPath.node.loc ?? GeneratedSource,
};
@@ -3428,6 +3435,7 @@ function lowerIdentifier(
kind: 'Identifier',
identifier: binding.identifier,
effect: Effect.Unknown,
abstractValue: null,
reactive: false,
loc: exprLoc,
};
@@ -3449,6 +3457,7 @@ function buildTemporaryPlace(builder: HIRBuilder, loc: SourceLocation): Place {
kind: 'Identifier',
identifier: builder.makeTemporary(loc),
effect: Effect.Unknown,
abstractValue: null,
reactive: false,
loc,
};
@@ -3511,6 +3520,7 @@ function lowerIdentifierForAssignment(
kind: 'Identifier',
identifier: binding.identifier,
effect: Effect.Unknown,
abstractValue: null,
reactive: false,
loc,
};

View File

@@ -761,6 +761,7 @@ function _staticInvariantInstructionValueHasLocation(
export type Phi = {
kind: 'Phi';
id: Identifier;
abstractValue: AbstractValue | null;
operands: Map<BlockId, Identifier>;
};
@@ -1110,6 +1111,7 @@ export type Place = {
kind: 'Identifier';
identifier: Identifier;
effect: Effect;
abstractValue: AbstractValue | null;
reactive: boolean;
loc: SourceLocation;
};

View File

@@ -895,6 +895,7 @@ export function createTemporaryPlace(
kind: 'Identifier',
identifier: makeTemporaryIdentifier(env.nextIdentifierId, loc),
reactive: false,
abstractValue: null,
effect: Effect.Unknown,
loc: GeneratedSource,
};

View File

@@ -86,6 +86,7 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
kind: 'Identifier',
identifier: phi.id,
effect: Effect.ConditionallyMutate,
abstractValue: null,
reactive: false,
loc: GeneratedSource,
},
@@ -95,6 +96,7 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
kind: 'Identifier',
identifier: operand,
effect: Effect.Read,
abstractValue: null,
reactive: false,
loc: GeneratedSource,
},

View File

@@ -833,6 +833,8 @@ export function printPattern(pattern: Pattern | Place | SpreadPattern): string {
export function printPlace(place: Place): string {
const items = [
place.abstractValue?.kind,
place.abstractValue ? ' ' : '',
place.effect,
' ',
printIdentifier(place.identifier),

View File

@@ -20,6 +20,7 @@ import {deadCodeElimination} from '../Optimization';
import {inferReactiveScopeVariables} from '../ReactiveScopes';
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
import {logHIRFunction} from '../Utils/logger';
import {inferFunctionEffects} from './InferFunctionEffects';
import {inferMutableContextVariables} from './InferMutableContextVariables';
import {inferMutableRanges} from './InferMutableRanges';
import inferReferenceEffects from './InferReferenceEffects';
@@ -106,7 +107,8 @@ export default function analyseFunctions(func: HIRFunction): void {
function lower(func: HIRFunction): void {
analyseFunctions(func);
inferReferenceEffects(func, {isFunctionExpression: true});
const aliases = inferReferenceEffects(func, {isFunctionExpression: true});
inferFunctionEffects(func, aliases, {isFunctionExpression: true});
deadCodeElimination(func);
inferMutableRanges(func);
rewriteInstructionKindsBasedOnReassignment(func);

View File

@@ -268,6 +268,7 @@ function getManualMemoizationReplacement(
kind: 'Identifier',
identifier: fn.identifier,
effect: Effect.Unknown,
abstractValue: null,
reactive: false,
loc,
},
@@ -420,6 +421,7 @@ export function dropManualMemoization(func: HIRFunction): void {
kind: 'Identifier',
identifier: fnPlace.identifier,
effect: Effect.Unknown,
abstractValue: null,
reactive: false,
loc: fnPlace.loc,
};

View File

@@ -9,29 +9,178 @@ import {CompilerError, ErrorSeverity, ValueKind} from '..';
import {
AbstractValue,
BasicBlock,
BlockId,
Effect,
Environment,
FunctionEffect,
HIRFunction,
IdentifierId,
Instruction,
InstructionValue,
Place,
ValueReason,
getHookKind,
isRefOrRefValue,
} from '../HIR';
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
import {
eachInstructionLValue,
eachInstructionOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import DisjointSet from '../Utils/DisjointSet';
import {assertExhaustive} from '../Utils/utils';
interface State {
kind(place: Place): AbstractValue;
values(place: Place): Array<InstructionValue>;
isDefined(place: Place): boolean;
type State = {
values: Map<IdentifierId, AbstractValue>;
nestedEffects: Map<IdentifierId, Set<FunctionEffect>>;
aliases: DisjointSet<IdentifierId>;
};
export function inferFunctionEffects(
fn: HIRFunction,
aliases: DisjointSet<IdentifierId>,
options: {isFunctionExpression: boolean} = {isFunctionExpression: false},
): void {
const state: State = {values: new Map(), nestedEffects: new Map(), aliases};
for (const param of fn.params) {
let place;
if (param.kind === 'Identifier') {
place = param;
} else {
place = param.place;
}
CompilerError.invariant(place.abstractValue != null, {
reason: 'Expected lvalue to have a kind',
loc: place.loc,
});
state.values.set(place.identifier.id, place.abstractValue);
}
// Build an environment mapping identifiers to AbstractValues
for (const cx of fn.context) {
CompilerError.invariant(cx.abstractValue != null, {
reason: 'Expected context to have a kind',
loc: cx.loc,
});
state.values.set(cx.identifier.id, cx.abstractValue);
}
let blocksSeen: Set<BlockId> = new Set();
let backEdgeSeen = false;
let lastNested: Map<IdentifierId, Set<FunctionEffect>>;
do {
lastNested = new Map(state.nestedEffects);
for (const [blockId, block] of fn.body.blocks) {
blocksSeen.add(blockId);
for (const phi of block.phis) {
CompilerError.invariant(phi.abstractValue != null, {
reason: 'Expected phi to have a kind',
loc: phi.id.loc,
});
const phiRoot = state.aliases.find(phi.id.id) ?? phi.id.id;
for (const [predId, operand] of phi.operands) {
if (!blocksSeen.has(predId)) {
backEdgeSeen = true;
}
const operandRoot = state.aliases.find(operand.id) ?? operand.id;
if (state.nestedEffects.has(operandRoot)) {
state.nestedEffects.set(
phiRoot,
state.nestedEffects.get(operandRoot) ?? new Set(),
);
}
}
state.values.set(phi.id.id, phi.abstractValue);
}
for (const instr of block.instructions) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
/**
* If this function references other functions, propagate the referenced function's
* effects to this function.
*
* ```
* let f = () => global = true;
* let g = () => f();
* g();
* ```
*
* In this example, because `g` references `f`, we propagate the GlobalMutation from
* `f` to `g`. Thus, referencing `g` in `g()` will evaluate the GlobalMutation in the outer
* function effect context and report an error. But if instead we do:
*
* ```
* let f = () => global = true;
* let g = () => f();
* useEffect(() => g(), [g])
* ```
*
* Now `g`'s effects will be discarded since they're in a useEffect.
*/
let propagatedEffects = [
...(instr.value.loweredFunc.func.effects ?? []),
];
for (const operand of eachInstructionOperand(instr)) {
propagatedEffects.push(
...inferFunctionInstrEffects(state, operand),
);
}
/*
* Since we infer the effects of nested functions from function
* instructions, we need to make sure we can go from identifiers
* aliased to function definitions to the function's effects
*/
const root =
state.aliases.find(instr.lvalue.identifier.id) ??
instr.lvalue.identifier.id;
CompilerError.invariant(root != null, {
reason: 'Expected lvalue to have a root',
loc: instr.loc,
});
const curEffects = state.nestedEffects.get(root) ?? new Set();
if (propagatedEffects.some(eff => !curEffects.has(eff))) {
const nested = new Set([...curEffects, ...propagatedEffects]);
state.nestedEffects.set(root, nested);
}
instr.value.loweredFunc.func.effects = propagatedEffects;
}
for (const lvalue of eachInstructionLValue(instr)) {
lvalue.abstractValue &&
state.values.set(lvalue.identifier.id, lvalue.abstractValue);
}
}
}
// Loop until we have propagated all nested effects backwards through backedges, if they exist
} while (
backEdgeSeen &&
![...state.nestedEffects].every(([id, effs]) => lastNested.get(id) === effs)
);
const functionEffects: Array<FunctionEffect> = [];
for (const [_, block] of fn.body.blocks) {
for (const instr of block.instructions) {
functionEffects.push(
...inferInstructionFunctionEffects(fn.env, state, instr),
);
}
functionEffects.push(...inferTerminalFunctionEffects(state, block));
}
if (options.isFunctionExpression) {
fn.effects = functionEffects;
} else {
raiseFunctionEffectErrors(functionEffects);
}
}
function inferOperandEffect(state: State, place: Place): null | FunctionEffect {
const value = state.kind(place);
const value = place.abstractValue;
CompilerError.invariant(value != null, {
reason: 'Expected operand to have a kind',
description: `${place.identifier.id}`,
loc: null,
});
@@ -82,7 +231,6 @@ function inheritFunctionEffects(
place: Place,
): Array<FunctionEffect> {
const effects = inferFunctionInstrEffects(state, place);
return effects
.flatMap(effect => {
if (effect.kind === 'GlobalMutation' || effect.kind === 'ReactMutation') {
@@ -106,11 +254,13 @@ function inheritFunctionEffects(
* more detailed effect to the current function context.
*/
for (const place of effect.places) {
if (state.isDefined(place)) {
const abstractValue = state.values.get(place.identifier.id);
if (abstractValue != null) {
const replayedEffect = inferOperandEffect(state, {
...place,
loc: effect.loc,
effect: effect.effect,
abstractValue,
});
if (replayedEffect != null) {
if (replayedEffect.kind === 'ContextMutation') {
@@ -120,7 +270,8 @@ function inheritFunctionEffects(
// Case 3, immutable value so propagate the more precise effect
effects.push(replayedEffect);
}
} // else case 2, local mutable value so this effect was fine
}
// else case 2, local mutable value so this effect was fine
}
}
return effects;
@@ -134,21 +285,13 @@ function inferFunctionInstrEffects(
place: Place,
): Array<FunctionEffect> {
const effects: Array<FunctionEffect> = [];
const instrs = state.values(place);
CompilerError.invariant(instrs != null, {
reason: 'Expected operand to have instructions',
loc: null,
const root = state.aliases.find(place.identifier.id) ?? place.identifier.id;
CompilerError.invariant(root != null, {
reason: 'Expected operand to have a root',
loc: place.loc,
});
for (const instr of instrs) {
if (
(instr.kind === 'FunctionExpression' || instr.kind === 'ObjectMethod') &&
instr.loweredFunc.func.effects != null
) {
effects.push(...instr.loweredFunc.func.effects);
}
}
const instrs = state.nestedEffects.get(root) ?? [];
effects.push(...instrs);
return effects;
}
@@ -193,34 +336,6 @@ export function inferInstructionFunctionEffects(
}
case 'ObjectMethod':
case 'FunctionExpression': {
/**
* If this function references other functions, propagate the referenced function's
* effects to this function.
*
* ```
* let f = () => global = true;
* let g = () => f();
* g();
* ```
*
* In this example, because `g` references `f`, we propagate the GlobalMutation from
* `f` to `g`. Thus, referencing `g` in `g()` will evaluate the GlobalMutation in the outer
* function effect context and report an error. But if instead we do:
*
* ```
* let f = () => global = true;
* let g = () => f();
* useEffect(() => g(), [g])
* ```
*
* Now `g`'s effects will be discarded since they're in a useEffect.
*/
for (const operand of eachInstructionOperand(instr)) {
instr.value.loweredFunc.func.effects ??= [];
instr.value.loweredFunc.func.effects.push(
...inferFunctionInstrEffects(state, operand),
);
}
break;
}
case 'MethodCall':
@@ -285,7 +400,7 @@ export function inferTerminalFunctionEffects(
return functionEffects;
}
export function raiseFunctionEffectErrors(
function raiseFunctionEffectErrors(
functionEffects: Array<FunctionEffect>,
): void {
functionEffects.forEach(eff => {

View File

@@ -13,15 +13,16 @@ import {
BlockId,
CallExpression,
Effect,
FunctionEffect,
GeneratedSource,
HIRFunction,
Identifier,
IdentifierId,
InstructionKind,
InstructionValue,
MethodCall,
Phi,
Place,
SourceLocation,
SpreadPattern,
Type,
ValueKind,
@@ -45,12 +46,8 @@ import {
eachTerminalOperand,
eachTerminalSuccessor,
} from '../HIR/visitors';
import DisjointSet from '../Utils/DisjointSet';
import {assertExhaustive} from '../Utils/utils';
import {
inferTerminalFunctionEffects,
inferInstructionFunctionEffects,
raiseFunctionEffectErrors,
} from './InferFunctionEffects';
const UndefinedValue: InstructionValue = {
kind: 'Primitive',
@@ -103,7 +100,7 @@ const UndefinedValue: InstructionValue = {
export default function inferReferenceEffects(
fn: HIRFunction,
options: {isFunctionExpression: boolean} = {isFunctionExpression: false},
): void {
): DisjointSet<IdentifierId> {
/*
* Initial state contains function params
* TODO: include module declarations here as well
@@ -127,12 +124,14 @@ export default function inferReferenceEffects(
properties: [],
loc: ref.loc,
};
initialState.initialize(value, {
const valueKind: AbstractValue = {
kind: ValueKind.Context,
reason: new Set([ValueReason.Other]),
context: new Set([ref]),
});
};
initialState.initialize(value, valueKind);
initialState.define(ref, value);
ref.abstractValue = valueKind;
}
const paramKind: AbstractValue = options.isFunctionExpression
@@ -177,12 +176,14 @@ export default function inferReferenceEffects(
loc: ref.place.loc,
};
}
initialState.initialize(value, {
const valueKind: AbstractValue = {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
});
};
initialState.initialize(value, valueKind);
initialState.define(place, value);
place.abstractValue = valueKind;
}
} else {
for (const param of fn.params) {
@@ -219,7 +220,7 @@ export default function inferReferenceEffects(
}
queue(fn.body.entry, initialState);
const functionEffects: Array<FunctionEffect> = fn.effects ?? [];
const finishedStates: Map<BlockId, InferenceState> = new Map();
while (queuedStates.size !== 0) {
for (const [blockId, block] of fn.body.blocks) {
@@ -231,19 +232,24 @@ export default function inferReferenceEffects(
statesByBlock.set(blockId, incomingState);
const state = incomingState.clone();
inferBlock(fn.env, state, block, functionEffects);
finishedStates.set(blockId, state);
inferBlock(fn.env, state, block);
for (const nextBlockId of eachTerminalSuccessor(block.terminal)) {
queue(nextBlockId, state);
}
}
}
CompilerError.invariant(finishedStates.size > 0, {
reason: 'Expected to have processed at least one block',
loc: null,
});
if (options.isFunctionExpression) {
fn.effects = functionEffects;
} else {
raiseFunctionEffectErrors(functionEffects);
}
const summaryState = Array(...finishedStates.values()).reduce(
(acc, state) => acc.merge(state) ?? acc,
);
return summaryState.aliases;
}
type FreezeAction = {values: Set<InstructionValue>; reason: Set<ValueReason>};
@@ -261,18 +267,26 @@ class InferenceState {
*/
#variables: Map<IdentifierId, Set<InstructionValue>>;
#aliases: DisjointSet<IdentifierId>;
constructor(
env: Environment,
values: Map<InstructionValue, AbstractValue>,
variables: Map<IdentifierId, Set<InstructionValue>>,
aliases: DisjointSet<IdentifierId>,
) {
this.#env = env;
this.#values = values;
this.#variables = variables;
this.#aliases = aliases;
}
get aliases(): DisjointSet<IdentifierId> {
return this.#aliases;
}
static empty(env: Environment): InferenceState {
return new InferenceState(env, new Map(), new Map());
return new InferenceState(env, new Map(), new Map(), new DisjointSet());
}
// (Re)initializes a @param value with its default @param kind.
@@ -290,7 +304,7 @@ class InferenceState {
values(place: Place): Array<InstructionValue> {
const values = this.#variables.get(place.identifier.id);
CompilerError.invariant(values != null, {
reason: `[hoisting] Expected value kind to be initialized`,
reason: `[hoisting] Expected value kind to be initialized in call to values()`,
description: `${printPlace(place)}`,
loc: place.loc,
suggestions: null,
@@ -299,11 +313,11 @@ class InferenceState {
}
// Lookup the kind of the given @param value.
kind(place: Place): AbstractValue {
kind(place: {identifier: Identifier; loc: SourceLocation}): AbstractValue {
const values = this.#variables.get(place.identifier.id);
CompilerError.invariant(values != null, {
reason: `[hoisting] Expected value kind to be initialized`,
description: `${printPlace(place)}`,
reason: `[hoisting] Expected value kind to be initialized in call to kind()`,
description: `${place.identifier.id}`,
loc: place.loc,
suggestions: null,
});
@@ -315,7 +329,7 @@ class InferenceState {
}
CompilerError.invariant(mergedKind !== null, {
reason: `InferReferenceEffects::kind: Expected at least one value`,
description: `No value found at \`${printPlace(place)}\``,
description: `No value found at \`${place.identifier.id}\``,
loc: place.loc,
suggestions: null,
});
@@ -332,6 +346,8 @@ class InferenceState {
suggestions: null,
});
this.#variables.set(place.identifier.id, new Set(values));
this.#aliases.union([place.identifier.id, value.identifier.id]);
place.abstractValue = value.abstractValue;
}
// Defines (initializing or updating) a variable with a specific kind of value.
@@ -370,6 +386,7 @@ class InferenceState {
reason: ValueReason,
): void {
const values = this.#variables.get(place.identifier.id);
let valueKind: AbstractValue = this.kind(place);
if (values === undefined) {
CompilerError.invariant(effectKind !== Effect.Store, {
reason: '[InferReferenceEffects] Unhandled store reference effect',
@@ -381,10 +398,12 @@ class InferenceState {
effectKind === Effect.ConditionallyMutate
? Effect.ConditionallyMutate
: Effect.Read;
place.abstractValue = valueKind;
return;
}
const action = this.reference(place, effectKind, reason);
place.abstractValue = valueKind;
action && freezeActions.push(action);
}
@@ -431,7 +450,7 @@ class InferenceState {
loc: place.loc,
suggestions: null,
});
let valueKind: AbstractValue | null = this.kind(place);
let valueKind: AbstractValue = this.kind(place);
let effect: Effect | null = null;
let freeze: null | FreezeAction = null;
switch (effectKind) {
@@ -539,6 +558,7 @@ class InferenceState {
merge(other: InferenceState): InferenceState | null {
let nextValues: Map<InstructionValue, AbstractValue> | null = null;
let nextVariables: Map<IdentifierId, Set<InstructionValue>> | null = null;
let nextAliases: DisjointSet<IdentifierId> | null = null;
for (const [id, thisValue] of this.#values) {
const otherValue = other.#values.get(id);
@@ -583,13 +603,21 @@ class InferenceState {
nextVariables.set(id, new Set(otherValues));
}
if (nextVariables === null && nextValues === null) {
if (!this.#aliases.equals(other.#aliases)) {
nextAliases = this.#aliases.copy();
for (const otherAliasSet of other.#aliases.buildSets()) {
nextAliases.union(Array(...otherAliasSet));
}
}
if (nextVariables === null && nextValues === null && nextAliases === null) {
return null;
} else {
return new InferenceState(
this.#env,
nextValues ?? new Map(this.#values),
nextVariables ?? new Map(this.#variables),
nextAliases ?? this.#aliases.copy(),
);
}
}
@@ -604,6 +632,7 @@ class InferenceState {
this.#env,
new Map(this.#values),
new Map(this.#variables),
this.#aliases.copy(),
);
}
@@ -634,10 +663,13 @@ class InferenceState {
inferPhi(phi: Phi): void {
const values: Set<InstructionValue> = new Set();
let valueKind;
for (const [_, operand] of phi.operands) {
const operandValues = this.#variables.get(operand.id);
// This is a backedge that will be handled later by State.merge
if (operandValues === undefined) continue;
const kind = this.kind({identifier: operand, loc: GeneratedSource});
valueKind = valueKind ? mergeAbstractValues(valueKind, kind) : kind;
for (const v of operandValues) {
values.add(v);
}
@@ -645,6 +677,7 @@ class InferenceState {
if (values.size > 0) {
this.#variables.set(phi.id.id, values);
phi.abstractValue = valueKind!;
}
}
}
@@ -673,6 +706,7 @@ function inferParam(
}
initialState.initialize(value, paramKind);
initialState.define(place, value);
place.abstractValue = paramKind;
}
/*
@@ -812,14 +846,11 @@ function mergeAbstractValues(
return {kind, reason, context};
}
type Continuation =
| {
kind: 'initialize';
valueKind: AbstractValue;
effect: {kind: Effect; reason: ValueReason} | null;
lvalueEffect?: Effect;
}
| {kind: 'funeffects'};
type Continuation = null | {
valueKind: AbstractValue;
effect: {kind: Effect; reason: ValueReason} | null;
lvalueEffect?: Effect;
};
/*
* Iterates over the given @param block, defining variables and
@@ -829,7 +860,6 @@ function inferBlock(
env: Environment,
state: InferenceState,
block: BasicBlock,
functionEffects: Array<FunctionEffect>,
): void {
for (const phi of block.phis) {
state.inferPhi(phi);
@@ -843,7 +873,6 @@ function inferBlock(
switch (instrValue.kind) {
case 'BinaryExpression': {
continuation = {
kind: 'initialize',
valueKind: {
kind: ValueKind.Primitive,
reason: new Set([ValueReason.Other]),
@@ -869,7 +898,6 @@ function inferBlock(
context: new Set(),
};
continuation = {
kind: 'initialize',
valueKind,
effect: {kind: Effect.Capture, reason: ValueReason.Other},
lvalueEffect: Effect.Store,
@@ -913,8 +941,9 @@ function inferBlock(
state.initialize(instrValue, valueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.abstractValue = valueKind;
instr.lvalue.effect = Effect.ConditionallyMutate;
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'ObjectExpression': {
@@ -972,13 +1001,13 @@ function inferBlock(
state.initialize(instrValue, valueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.abstractValue = valueKind;
instr.lvalue.effect = Effect.Store;
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'UnaryExpression': {
continuation = {
kind: 'initialize',
valueKind: {
kind: ValueKind.Primitive,
reason: new Set([ValueReason.Other]),
@@ -991,7 +1020,6 @@ function inferBlock(
case 'UnsupportedNode': {
// TODO: handle other statement kinds
continuation = {
kind: 'initialize',
valueKind: {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
@@ -1038,19 +1066,20 @@ function inferBlock(
}
}
state.initialize(instrValue, {
const valueKind: AbstractValue = {
kind: ValueKind.Frozen,
reason: new Set([ValueReason.Other]),
context: new Set(),
});
};
state.initialize(instrValue, valueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.abstractValue = valueKind;
instr.lvalue.effect = Effect.ConditionallyMutate;
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'JsxFragment': {
continuation = {
kind: 'initialize',
valueKind: {
kind: ValueKind.Frozen,
reason: new Set([ValueReason.Other]),
@@ -1069,7 +1098,6 @@ function inferBlock(
* an immutable string
*/
continuation = {
kind: 'initialize',
valueKind: {
kind: ValueKind.Primitive,
reason: new Set([ValueReason.Other]),
@@ -1082,7 +1110,6 @@ function inferBlock(
case 'RegExpLiteral': {
// RegExp instances are mutable objects
continuation = {
kind: 'initialize',
valueKind: {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
@@ -1097,11 +1124,10 @@ function inferBlock(
}
case 'MetaProperty': {
if (instrValue.meta !== 'import' || instrValue.property !== 'meta') {
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
continuation = {
kind: 'initialize',
valueKind: {
kind: ValueKind.Global,
reason: new Set([ValueReason.Global]),
@@ -1113,7 +1139,6 @@ function inferBlock(
}
case 'LoadGlobal':
continuation = {
kind: 'initialize',
valueKind: {
kind: ValueKind.Global,
reason: new Set([ValueReason.Global]),
@@ -1126,7 +1151,6 @@ function inferBlock(
case 'JSXText':
case 'Primitive': {
continuation = {
kind: 'initialize',
valueKind: {
kind: ValueKind.Primitive,
reason: new Set([ValueReason.Other]),
@@ -1156,14 +1180,16 @@ function inferBlock(
* If a closure did not capture any mutable values, then we can consider it to be
* frozen, which allows it to be independently memoized.
*/
state.initialize(instrValue, {
const valueKind: AbstractValue = {
kind: hasMutableOperand ? ValueKind.Mutable : ValueKind.Frozen,
reason: new Set([ValueReason.Other]),
context: new Set(),
});
};
state.initialize(instrValue, valueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.abstractValue = valueKind;
instr.lvalue.effect = Effect.Store;
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'TaggedTemplateExpression': {
@@ -1204,8 +1230,9 @@ function inferBlock(
);
state.initialize(instrValue, returnValueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.abstractValue = returnValueKind;
instr.lvalue.effect = Effect.ConditionallyMutate;
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'CallExpression': {
@@ -1271,10 +1298,11 @@ function inferBlock(
state.initialize(instrValue, returnValueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.abstractValue = returnValueKind;
instr.lvalue.effect = hasCaptureArgument
? Effect.Store
: Effect.ConditionallyMutate;
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'MethodCall': {
@@ -1336,11 +1364,12 @@ function inferBlock(
);
state.initialize(instrValue, returnValueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.abstractValue = returnValueKind;
instr.lvalue.effect =
instrValue.receiver.effect === Effect.Capture
? Effect.Store
: Effect.ConditionallyMutate;
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
@@ -1390,10 +1419,11 @@ function inferBlock(
state.initialize(instrValue, returnValueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.abstractValue = returnValueKind;
instr.lvalue.effect = hasCaptureArgument
? Effect.Store
: Effect.ConditionallyMutate;
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'PropertyStore': {
@@ -1417,13 +1447,12 @@ function inferBlock(
const lvalue = instr.lvalue;
state.alias(lvalue, instrValue.value);
lvalue.effect = Effect.Store;
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'PropertyDelete': {
// `delete` returns a boolean (immutable) and modifies the object
continuation = {
kind: 'initialize',
valueKind: {
kind: ValueKind.Primitive,
reason: new Set([ValueReason.Other]),
@@ -1441,10 +1470,12 @@ function inferBlock(
ValueReason.Other,
);
const lvalue = instr.lvalue;
const valueKind = state.kind(instrValue.object);
lvalue.effect = Effect.ConditionallyMutate;
state.initialize(instrValue, state.kind(instrValue.object));
state.initialize(instrValue, valueKind);
state.define(lvalue, instrValue);
continuation = {kind: 'funeffects'};
instr.lvalue.abstractValue = valueKind;
continuation = null;
break;
}
case 'ComputedStore': {
@@ -1474,7 +1505,7 @@ function inferBlock(
const lvalue = instr.lvalue;
state.alias(lvalue, instrValue.value);
lvalue.effect = Effect.Store;
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'ComputedDelete': {
@@ -1490,14 +1521,16 @@ function inferBlock(
Effect.Read,
ValueReason.Other,
);
state.initialize(instrValue, {
const valueKind: AbstractValue = {
kind: ValueKind.Primitive,
reason: new Set([ValueReason.Other]),
context: new Set(),
});
};
state.initialize(instrValue, valueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.abstractValue = valueKind;
instr.lvalue.effect = Effect.Mutate;
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'ComputedLoad': {
@@ -1513,11 +1546,13 @@ function inferBlock(
Effect.Read,
ValueReason.Other,
);
const valueKind = state.kind(instrValue.object);
const lvalue = instr.lvalue;
lvalue.effect = Effect.ConditionallyMutate;
state.initialize(instrValue, state.kind(instrValue.object));
state.initialize(instrValue, valueKind);
state.define(lvalue, instrValue);
continuation = {kind: 'funeffects'};
instr.lvalue.abstractValue = valueKind;
continuation = null;
break;
}
case 'Await': {
@@ -1536,7 +1571,7 @@ function inferBlock(
const lvalue = instr.lvalue;
lvalue.effect = Effect.ConditionallyMutate;
state.alias(lvalue, instrValue.value);
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'TypeCastExpression': {
@@ -1558,7 +1593,7 @@ function inferBlock(
const lvalue = instr.lvalue;
lvalue.effect = Effect.ConditionallyMutate;
state.alias(lvalue, instrValue.value);
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'StartMemoize':
@@ -1580,15 +1615,17 @@ function inferBlock(
);
}
}
const lvalue = instr.lvalue;
lvalue.effect = Effect.ConditionallyMutate;
state.initialize(instrValue, {
const valueKind: AbstractValue = {
kind: ValueKind.Frozen,
reason: new Set([ValueReason.Other]),
context: new Set(),
});
};
const lvalue = instr.lvalue;
lvalue.effect = Effect.ConditionallyMutate;
state.initialize(instrValue, valueKind);
state.define(lvalue, instrValue);
continuation = {kind: 'funeffects'};
instr.lvalue.abstractValue = valueKind;
continuation = null;
break;
}
case 'LoadLocal': {
@@ -1607,7 +1644,7 @@ function inferBlock(
lvalue.effect = Effect.ConditionallyMutate;
// direct aliasing: `a = b`;
state.alias(lvalue, instrValue.place);
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'LoadContext': {
@@ -1622,13 +1659,13 @@ function inferBlock(
const valueKind = state.kind(instrValue.place);
state.initialize(instrValue, valueKind);
state.define(lvalue, instrValue);
continuation = {kind: 'funeffects'};
instr.lvalue.abstractValue = valueKind;
continuation = null;
break;
}
case 'DeclareLocal': {
const value = UndefinedValue;
state.initialize(
value,
const valueKind: AbstractValue =
// Catch params may be aliased to mutable values
instrValue.lvalue.kind === InstructionKind.Catch
? {
@@ -1640,20 +1677,27 @@ function inferBlock(
kind: ValueKind.Primitive,
reason: new Set([ValueReason.Other]),
context: new Set(),
},
);
};
state.initialize(value, valueKind);
state.define(instrValue.lvalue.place, value);
continuation = {kind: 'funeffects'};
instrValue.lvalue.place.abstractValue = valueKind;
state.define(instr.lvalue, value);
instr.lvalue.abstractValue = valueKind;
continuation = null;
break;
}
case 'DeclareContext': {
state.initialize(instrValue, {
const valueKind: AbstractValue = {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
});
};
state.initialize(instrValue, valueKind);
state.define(instrValue.lvalue.place, instrValue);
continuation = {kind: 'funeffects'};
instrValue.lvalue.place.abstractValue = valueKind;
state.define(instr.lvalue, instrValue);
instr.lvalue.abstractValue = valueKind;
continuation = null;
break;
}
case 'PostfixUpdate':
@@ -1681,7 +1725,7 @@ function inferBlock(
* replacing it
*/
instrValue.lvalue.effect = Effect.Store;
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'StoreLocal': {
@@ -1708,7 +1752,7 @@ function inferBlock(
* replacing it
*/
instrValue.lvalue.place.effect = Effect.Store;
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'StoreContext': {
@@ -1728,7 +1772,7 @@ function inferBlock(
const lvalue = instr.lvalue;
state.alias(lvalue, instrValue.value);
lvalue.effect = Effect.Store;
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'StoreGlobal': {
@@ -1740,7 +1784,11 @@ function inferBlock(
);
const lvalue = instr.lvalue;
lvalue.effect = Effect.Store;
continuation = {kind: 'funeffects'};
const valueKind = state.kind(instrValue.value);
state.initialize(instrValue, valueKind);
state.define(lvalue, instrValue);
instr.lvalue.abstractValue = valueKind;
continuation = null;
break;
}
case 'Destructure': {
@@ -1774,7 +1822,7 @@ function inferBlock(
*/
place.effect = Effect.Store;
}
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'GetIterator': {
@@ -1817,7 +1865,6 @@ function inferBlock(
valueKind = state.kind(instrValue.collection);
}
continuation = {
kind: 'initialize',
effect,
valueKind,
lvalueEffect: Effect.Store,
@@ -1853,15 +1900,16 @@ function inferBlock(
Effect.Capture,
ValueReason.Other,
);
state.initialize(instrValue, state.kind(instrValue.collection));
const valueKind = state.kind(instrValue.collection);
state.initialize(instrValue, valueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.abstractValue = valueKind;
instr.lvalue.effect = Effect.Store;
continuation = {kind: 'funeffects'};
continuation = null;
break;
}
case 'NextPropertyOf': {
continuation = {
kind: 'initialize',
effect: {kind: Effect.Read, reason: ValueReason.Other},
lvalueEffect: Effect.Store,
valueKind: {
@@ -1877,7 +1925,7 @@ function inferBlock(
}
}
if (continuation.kind === 'initialize') {
if (continuation !== null) {
for (const operand of eachInstructionOperand(instr)) {
CompilerError.invariant(continuation.effect != null, {
reason: `effectKind must be set for instruction value \`${instrValue.kind}\``,
@@ -1895,10 +1943,10 @@ function inferBlock(
state.initialize(instrValue, continuation.valueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.abstractValue = continuation.valueKind;
instr.lvalue.effect = continuation.lvalueEffect ?? defaultLvalueEffect;
}
functionEffects.push(...inferInstructionFunctionEffects(env, state, instr));
freezeActions.forEach(({values, reason}) =>
state.freezeValues(values, reason),
);
@@ -1926,7 +1974,6 @@ function inferBlock(
ValueReason.Other,
);
}
functionEffects.push(...inferTerminalFunctionEffects(state, block));
terminalFreezeActions.forEach(({values, reason}) =>
state.freezeValues(values, reason),
);

View File

@@ -237,6 +237,7 @@ class Transform extends ReactiveFunctionTransform<State> {
place: {
kind: 'Identifier',
effect: Effect.ConditionallyMutate,
abstractValue: null,
loc,
reactive: true,
identifier: earlyReturnValue.value,
@@ -308,6 +309,7 @@ class Transform extends ReactiveFunctionTransform<State> {
kind: 'Identifier',
identifier: earlyReturnValue.value,
effect: Effect.Capture,
abstractValue: null,
loc,
reactive: true,
},

View File

@@ -191,6 +191,7 @@ class SSABuilder {
const phi: Phi = {
kind: 'Phi',
id: newId,
abstractValue: null,
operands: predDefs,
};

View File

@@ -124,6 +124,54 @@ export default class DisjointSet<T> {
return [...sets.values()];
}
copy(): DisjointSet<T> {
const copy = new DisjointSet<T>();
copy.#entries = new Map(this.#entries);
return copy;
}
equals(other: DisjointSet<T>): boolean {
if (this.size !== other.size) {
return false;
}
const rootMap = new Map<T, T>();
for (const thisGroupId of this.#entries.values()) {
const otherGroupId = other.find(thisGroupId);
if (otherGroupId === null || this.find(otherGroupId) !== thisGroupId) {
return false;
}
rootMap.set(thisGroupId, otherGroupId);
}
for (const otherGroupId of other.#entries.values()) {
if (!new Set(rootMap.values()).has(otherGroupId)) {
return false;
}
}
for (const item of this.#entries.keys()) {
const otherRoot = other.find(item);
if (otherRoot === null) {
return false;
}
const thisRoot = this.find(item);
CompilerError.invariant(thisRoot != null, {
reason: 'Expected item to be in set',
loc: null,
});
if (rootMap.get(thisRoot) !== otherRoot) {
return false;
}
}
for (const item of other.#entries.keys()) {
const thisRoot = this.find(item);
if (thisRoot === null) {
return false;
}
}
return true;
}
get size(): number {
return this.#entries.size;
}

View File

@@ -250,6 +250,7 @@ function validateInferredDep(
identifier: dep.identifier,
loc: GeneratedSource,
effect: Effect.Read,
abstractValue: null,
reactive: false,
},
},

View File

@@ -116,4 +116,45 @@ describe('DisjointSet', () => {
identifiers.forEach((_, group) => expect(group).toBe(z));
});
it('`.equals` is false when it should be', () => {
const x = new DisjointSet<TestIdentifier>();
const y = new DisjointSet<TestIdentifier>();
const [a, b] = makeIdentifiers('a', 'b');
x.union([a, b]);
y.union([a]);
y.union([b]);
expect(x.equals(y)).toBe(false);
expect(y.equals(x)).toBe(false);
});
it('`.equals` is true when it should be', () => {
const x = new DisjointSet<TestIdentifier>();
const y = new DisjointSet<TestIdentifier>();
const [a, b, c] = makeIdentifiers('a', 'b', 'c');
x.union([a, b, c]);
y.union([a, b]);
y.union([b, c]);
expect(x.equals(y)).toBe(true);
expect(y.equals(x)).toBe(true);
});
it('`.copy` doesnt mutate the underlying', () => {
const x = new DisjointSet<TestIdentifier>();
const [a, b] = makeIdentifiers('a', 'b');
x.union([a]);
x.union([b]);
const y = x.copy();
y.union([a, b]);
expect(x.find(a) !== x.find(b)).toBe(true);
expect(y.find(a) === y.find(b)).toBe(true);
});
});

View File

@@ -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$16 (9:9)
| ^^^^^^^^^ Invariant: [hoisting] Expected value kind to be initialized in call to kind(). 16 (9:9)
10 | }
11 |
```