@samitouri / QOS-React-2 / commits / a2af8776a4

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 +}