Add InferMutableContextVariables (unused)
New pass to infer context variables which are definitively mutated, distinguishing from context variables which _may_ have been mutated. See the code comment on the new pass for more details.
Joe Savona committed
Jun 16, 2023 at 14:42 UTC
a2af8776a44d1a621f5380155adcc9fa9f25fb9d
2 files changed
+126
-3
compiler/forget/packages/babel-plugin-react-forget/src/Inference/AnalyseFunctions.ts
+14
-3
@@ -24,9 +24,20 @@ import { logHIRFunction } from "../Utils/logger";
24
import { inferMutableRanges } from "./InferMutableRanges";
25
import inferReferenceEffects from "./InferReferenceEffects";
26
27
-class State {
27
+/**
28
+ * Helper class to track indirections such as LoadLocal and PropertyLoad.
29
+ */
30
+export class IdentifierState {
31
properties: Map<Identifier, ReactiveScopeDependency> = new Map();
32
33
+ resolve(identifier: Identifier): Identifier {
34
+ const resolved = this.properties.get(identifier);
35
+ if (resolved !== undefined) {
36
+ return resolved.identifier;
37
+ }
38
+ return identifier;
39
+ }
40
+
41
declareProperty(lvalue: Place, object: Place, property: string): void {
42
const objectDependency = this.properties.get(object.identifier);
43
let nextDependency: ReactiveScopeDependency;
@@ -53,7 +64,7 @@ class State {
64
}
65
66
export default function analyseFunctions(func: HIRFunction): void {
56
- const state = new State();
67
+ const state = new IdentifierState();
68
69
for (const [_, block] of func.body.blocks) {
70
for (const instr of block.instructions) {
@@ -106,7 +117,7 @@ function lower(func: HIRFunction): void {
117
118
function infer(
119
value: FunctionExpression,
109
- state: State,
120
+ state: IdentifierState,
121
context: Place[]
122
): void {
123
const mutations = new Set(
compiler/forget/packages/babel-plugin-react-forget/src/Inference/InferMutableContextVariables.ts
new
+112
@@ -0,0 +1,112 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import { Effect, HIRFunction, Identifier, Place } from "../HIR";
9
+import {
10
+ eachInstructionValueOperand,
11
+ eachTerminalOperand,
12
+} from "../HIR/visitors";
13
+import { IdentifierState } from "./AnalyseFunctions";
14
+
15
+/**
16
+ * This pass infers which of the given function's context (free) variables
17
+ * are definitively mutated by the function. This analysis is *partial*,
18
+ * and only annotates provable mutations, and may miss mutations via indirections.
19
+ * The intent of this pass is to drive validations, rejecting known-bad code
20
+ * while avoiding false negatives, and the inference should *not* be used to
21
+ * drive changes in output.
22
+ *
23
+ * Note that a complete analysis is possible but would have too many false negatives.
24
+ * The approach would be to run LeaveSSA and InferReactiveScopeVariables in order to
25
+ * find all possible aliases of a context variable which may be mutated. However, this
26
+ * can lead to false negatives:
27
+ *
28
+ * ```
29
+ * const [x, setX] = useState(null); // x is frozen
30
+ * const fn = () => { // context=[x]
31
+ * const z = {}; // z is mutable
32
+ * foo(z, x); // potentially mutate z and x
33
+ * z.a = true; // definitively mutate z
34
+ * }
35
+ * fn();
36
+ * ```
37
+ *
38
+ * When we analyze function expressions we assume that context variables are mutable,
39
+ * so we assume that `x` is mutable. We infer that `foo(z, x)` could be mutating the
40
+ * two variables to alias each other, such that `z.a = true` could be mutating `x`,
41
+ * and we would infer that `x` is definitively mutated. Then when we run
42
+ * InferReferenceEffects on the outer code we'd reject it, since there is a definitive
43
+ * mutation of a frozen value.
44
+ *
45
+ * Thus the actual implementation looks at only basic aliasing. The above example would
46
+ * pass, since z does not directly alias `x`. However, mutations through trivial aliases
47
+ * are detected:
48
+ *
49
+ * ```
50
+ * const [x, setX] = useState(null); // x is frozen
51
+ * const fn = () => { // context=[x]
52
+ * const z = x;
53
+ * z.a = true; // ERROR: mutates x
54
+ * }
55
+ * fn();
56
+ * ```
57
+ */
58
+export function inferMutableContextVariables(fn: HIRFunction): void {
59
+ const state = new IdentifierState();
60
+ const knownMutatedIdentifiers = new Set<Identifier>();
61
+ for (const [, block] of fn.body.blocks) {
62
+ for (const instr of block.instructions) {
63
+ switch (instr.value.kind) {
64
+ case "PropertyLoad": {
65
+ state.declareProperty(
66
+ instr.lvalue,
67
+ instr.value.object,
68
+ instr.value.property
69
+ );
70
+ break;
71
+ }
72
+ case "ComputedLoad": {
73
+ // The path is set to an empty string as the path doesn't really
74
+ // matter for a computed load.
75
+ state.declareProperty(instr.lvalue, instr.value.object, "");
76
+ break;
77
+ }
78
+ case "LoadLocal":
79
+ case "LoadContext": {
80
+ if (instr.lvalue.identifier.name === null) {
81
+ state.declareTemporary(instr.lvalue, instr.value.place);
82
+ }
83
+ break;
84
+ }
85
+ default: {
86
+ for (const operand of eachInstructionValueOperand(instr.value)) {
87
+ visitOperand(state, knownMutatedIdentifiers, operand);
88
+ }
89
+ }
90
+ }
91
+ }
92
+ for (const operand of eachTerminalOperand(block.terminal)) {
93
+ visitOperand(state, knownMutatedIdentifiers, operand);
94
+ }
95
+ }
96
+ for (const operand of fn.context) {
97
+ if (knownMutatedIdentifiers.has(operand.identifier)) {
98
+ operand.effect = Effect.Mutate;
99
+ }
100
+ }
101
+}
102
+
103
+function visitOperand(
104
+ state: IdentifierState,
105
+ knownMutatedIdentifiers: Set<Identifier>,
106
+ operand: Place
107
+): void {
108
+ const resolved = state.resolve(operand.identifier);
109
+ if (operand.effect === Effect.Mutate || operand.effect === Effect.Store) {
110
+ knownMutatedIdentifiers.add(resolved);
111
+ }
112
+}