Files
react/compiler/forget/src/Inference/AnalyseFunctions.ts
T
Mofei Zhang 314a5cfca5 [hir] Add Load/StoreContext (1/n)
--- 

This PR adds LoadContext and StoreContext to handle reading and writing to 
context variables. 

A context variable is any variable that is declared within a Forget-compiled 
function and reassigned within a closure. Conceptually, we want to treat these 
variables as attributes of a `EnvironmentContext` variable (as most javascript 
VMs do). 

- context variables currently do not participate in type inference (i.e. we do 
not produce type equations for loads from context variables). In the future, we 
can try typing this as `Phi(assignment1Type, assignment2Type, ...)`. 

- context variables are always treated as `Effect.Mutable`. 

- context variables do not participate in SSA, or certain optimizing passes 
(e.g. dead code elimination, constant propagation, etc). 

There is some still follow ups: 

- From my understanding, we should introduce a `DeclareContext` instruction. 

- currently, declaring a context variable (without initializing it) is broken. 
This is because the declaration lowers to `DeclareLocal`, which assumes it is 
storing to a SSA-fied identifier. 

```js 

let x; 

x = 4; 

() => { x = {}; }; 

``` 

- DeclareContext will also make some initialization logic easier. In this PR, I 
added some hack-y code to handle initializing effects / mutable ranges / other 
inference state for the first StoreContext. 

- Handle or bail on stores to context variables through destructuring assignment 

- 

~~Next PR:~~ 

- ~~Change closures to track reassigned identifiers (to extend mutable range of 
primitives)~~
2023-05-12 12:17:05 -04:00

157 lines
4.6 KiB
TypeScript

/**
* 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 invariant from "invariant";
import {
Effect,
FunctionExpression,
HIRFunction,
Identifier,
mergeConsecutiveBlocks,
Place,
ReactiveScopeDependency,
} from "../HIR";
import { constantPropagation } from "../Optimization";
import { eliminateRedundantPhi, enterSSA } from "../SSA";
import { inferTypes } from "../TypeInference";
import { logHIRFunction } from "../Utils/logger";
import { inferMutableRanges } from "./InferMutableRanges";
import inferReferenceEffects from "./InferReferenceEffects";
class State {
properties: Map<Identifier, ReactiveScopeDependency> = new Map();
declareProperty(lvalue: Place, object: Place, property: string): void {
const objectDependency = this.properties.get(object.identifier);
let nextDependency: ReactiveScopeDependency;
if (objectDependency === undefined) {
nextDependency = { identifier: object.identifier, path: [property] };
} else {
nextDependency = {
identifier: objectDependency.identifier,
path: [...objectDependency.path, property],
};
}
this.properties.set(lvalue.identifier, nextDependency);
}
declareTemporary(lvalue: Place, value: Place): void {
const resolved: ReactiveScopeDependency = this.properties.get(
value.identifier
) ?? {
identifier: value.identifier,
path: [],
};
this.properties.set(lvalue.identifier, resolved);
}
}
export default function analyseFunctions(func: HIRFunction): void {
const state = new State();
for (const [_, block] of func.body.blocks) {
for (const instr of block.instructions) {
switch (instr.value.kind) {
case "FunctionExpression": {
lower(instr.value.loweredFunc);
infer(instr.value, state, func.context);
break;
}
case "PropertyLoad": {
state.declareProperty(
instr.lvalue,
instr.value.object,
instr.value.property
);
break;
}
case "ComputedLoad": {
// The path is set to an empty string as the path doesn't really
// matter for a computed load.
state.declareProperty(instr.lvalue, instr.value.object, "");
break;
}
case "LoadLocal":
case "LoadContext": {
if (instr.lvalue.identifier.name === null) {
state.declareTemporary(instr.lvalue, instr.value.place);
}
break;
}
}
}
}
}
function lower(func: HIRFunction): void {
mergeConsecutiveBlocks(func);
enterSSA(func);
eliminateRedundantPhi(func);
constantPropagation(func);
inferTypes(func);
analyseFunctions(func);
inferReferenceEffects(func);
inferMutableRanges(func);
logHIRFunction("AnalyseFunction (inner)", func);
}
function infer(
value: FunctionExpression,
state: State,
context: Place[]
): void {
const mutations = new Set(
value.loweredFunc.context
.filter((dep) => isMutatedOrReassigned(dep.identifier))
.map((m) => m.identifier.name)
.filter((m) => m !== null) as string[]
);
for (const dep of value.dependencies) {
let name: string | null = null;
if (state.properties.has(dep.identifier)) {
const receiver = state.properties.get(dep.identifier)!;
name = receiver.identifier.name;
} else {
name = dep.identifier.name;
}
if (name !== null && mutations.has(name)) {
dep.effect = Effect.Capture;
}
}
// This could potentially add duplicate deps to mutatedDeps in the case of
// mutating a context ref in the child function and in this parent function.
// It might be useful to dedupe this.
//
// In practice this never really matters because the Component function has no
// context refs, so it will never have duplicate deps.
for (const place of context) {
invariant(
place.identifier.name !== null,
"context refs should always have a name"
);
if (mutations.has(place.identifier.name)) {
place.effect = Effect.Capture;
value.dependencies.push(place);
}
}
}
function isMutatedOrReassigned(id: Identifier): boolean {
// This check checks for mutation and reassingnment, so the usual check for
// mutation (ie, `mutableRange.end - mutableRange.start > 1`) isn't quite
// enough.
//
// We need to track re-assignments in context refs as we need to reflect the
// re-assignment back to the captured refs.
return id.mutableRange.end > id.mutableRange.start;
}