Skip to content

Commit e7d1fff

Browse files
committed
[compiler] Add lowerContextAccess pass
*This is only for internal profiling, not intended to ship.* This pass is intended to be used with #30407. This pass synthesizes selector functions by collecting immediately destructured context acesses. We bailout for other types of context access. This pass lowers context access to use a selector function by passing the synthesized selector function as the second argument. ghstack-source-id: 0cefe6c Pull Request resolved: #30548
1 parent 9899bb0 commit e7d1fff

10 files changed

+495
-0
lines changed

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ import {
9898
} from '../Validation';
9999
import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
100100
import {outlineFunctions} from '../Optimization/OutlineFunctions';
101+
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
101102

102103
export type CompilerPipelineValue =
103104
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -199,6 +200,10 @@ function* runWithEnvironment(
199200
validateNoCapitalizedCalls(hir);
200201
}
201202

203+
if (env.config.enableLowerContextAccess) {
204+
lowerContextAccess(hir);
205+
}
206+
202207
analyseFunctions(hir);
203208
yield log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
204209

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
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 {
9+
ArrayExpression,
10+
BasicBlock,
11+
CallExpression,
12+
Destructure,
13+
Effect,
14+
Environment,
15+
GeneratedSource,
16+
HIRFunction,
17+
IdentifierId,
18+
Instruction,
19+
LoadLocal,
20+
Place,
21+
PropertyLoad,
22+
isUseContextHookType,
23+
makeBlockId,
24+
makeIdentifierId,
25+
makeIdentifierName,
26+
makeInstructionId,
27+
makeTemporary,
28+
makeType,
29+
markInstructionIds,
30+
mergeConsecutiveBlocks,
31+
removeUnnecessaryTryCatch,
32+
reversePostorderBlocks,
33+
} from '../HIR';
34+
import {
35+
removeDeadDoWhileStatements,
36+
removeUnreachableForUpdates,
37+
} from '../HIR/HIRBuilder';
38+
import {enterSSA} from '../SSA';
39+
import {inferTypes} from '../TypeInference';
40+
41+
export function lowerContextAccess(fn: HIRFunction): void {
42+
const contextAccess: Map<IdentifierId, CallExpression> = new Map();
43+
const contextKeys: Map<IdentifierId, Array<string>> = new Map();
44+
45+
// collect context access and keys
46+
for (const [, block] of fn.body.blocks) {
47+
for (const instr of block.instructions) {
48+
const {value, lvalue} = instr;
49+
50+
if (
51+
value.kind === 'CallExpression' &&
52+
isUseContextHookType(value.callee.identifier)
53+
) {
54+
contextAccess.set(lvalue.identifier.id, value);
55+
continue;
56+
}
57+
58+
if (value.kind !== 'Destructure') {
59+
continue;
60+
}
61+
62+
const destructureId = value.value.identifier.id;
63+
if (!contextAccess.has(destructureId)) {
64+
continue;
65+
}
66+
67+
const keys = getContextKeys(value);
68+
if (keys === null) {
69+
continue;
70+
}
71+
72+
if (contextKeys.has(destructureId)) {
73+
/*
74+
* TODO(gsn): Add support for accessing context over multiple
75+
* statements.
76+
*/
77+
return;
78+
} else {
79+
contextKeys.set(destructureId, keys);
80+
}
81+
}
82+
}
83+
84+
if (contextAccess.size > 0) {
85+
for (const [, block] of fn.body.blocks) {
86+
const nextInstructions: Array<Instruction> = [];
87+
for (const instr of block.instructions) {
88+
const {lvalue, value} = instr;
89+
if (
90+
value.kind === 'CallExpression' &&
91+
isUseContextHookType(value.callee.identifier) &&
92+
contextKeys.has(lvalue.identifier.id)
93+
) {
94+
const keys = contextKeys.get(lvalue.identifier.id)!;
95+
const selectorFnInstr = emitSelectorFn(fn.env, keys);
96+
nextInstructions.push(selectorFnInstr);
97+
98+
const selectorFn = selectorFnInstr.lvalue;
99+
value.args.push(selectorFn);
100+
}
101+
102+
nextInstructions.push(instr);
103+
}
104+
block.instructions = nextInstructions;
105+
}
106+
markInstructionIds(fn.body);
107+
}
108+
}
109+
110+
function getContextKeys(value: Destructure): Array<string> | null {
111+
const keys = [];
112+
const pattern = value.lvalue.pattern;
113+
114+
switch (pattern.kind) {
115+
case 'ArrayPattern': {
116+
for (const place of pattern.items) {
117+
if (place.kind !== 'Identifier') {
118+
return null;
119+
}
120+
121+
if (place.identifier.name === null) {
122+
return null;
123+
}
124+
125+
keys.push(place.identifier.name.value);
126+
}
127+
return keys;
128+
}
129+
130+
case 'ObjectPattern': {
131+
for (const place of pattern.properties) {
132+
if (
133+
place.kind !== 'ObjectProperty' ||
134+
place.type !== 'property' ||
135+
place.key.kind !== 'identifier'
136+
) {
137+
return null;
138+
}
139+
keys.push(place.key.name);
140+
}
141+
return keys;
142+
}
143+
}
144+
}
145+
146+
function emitPropertyLoad(
147+
env: Environment,
148+
obj: Place,
149+
property: string,
150+
): {instructions: Array<Instruction>; element: Place} {
151+
const loadObj: LoadLocal = {
152+
kind: 'LoadLocal',
153+
place: obj,
154+
loc: GeneratedSource,
155+
};
156+
const object: Place = {
157+
kind: 'Identifier',
158+
identifier: makeTemporary(env.nextIdentifierId, GeneratedSource),
159+
effect: Effect.Unknown,
160+
reactive: false,
161+
loc: GeneratedSource,
162+
};
163+
const loadLocalInstr: Instruction = {
164+
lvalue: object,
165+
value: loadObj,
166+
id: makeInstructionId(0),
167+
loc: GeneratedSource,
168+
};
169+
170+
const loadProp: PropertyLoad = {
171+
kind: 'PropertyLoad',
172+
object,
173+
property,
174+
loc: GeneratedSource,
175+
};
176+
const element: Place = {
177+
kind: 'Identifier',
178+
identifier: makeTemporary(env.nextIdentifierId, GeneratedSource),
179+
effect: Effect.Unknown,
180+
reactive: false,
181+
loc: GeneratedSource,
182+
};
183+
const loadPropInstr: Instruction = {
184+
lvalue: element,
185+
value: loadProp,
186+
id: makeInstructionId(0),
187+
loc: GeneratedSource,
188+
};
189+
return {
190+
instructions: [loadLocalInstr, loadPropInstr],
191+
element: element,
192+
};
193+
}
194+
195+
function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
196+
const obj: Place = {
197+
kind: 'Identifier',
198+
identifier: {
199+
id: makeIdentifierId(env.nextIdentifierId),
200+
name: makeIdentifierName('c'),
201+
mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)},
202+
scope: null,
203+
type: makeType(),
204+
loc: GeneratedSource,
205+
},
206+
effect: Effect.Unknown,
207+
reactive: false,
208+
loc: GeneratedSource,
209+
};
210+
const instr: Array<Instruction> = [];
211+
const elements = [];
212+
for (const key of keys) {
213+
const {instructions, element: prop} = emitPropertyLoad(env, obj, key);
214+
instr.push(...instructions);
215+
elements.push(prop);
216+
}
217+
218+
const arrayInstr = emitArrayInstr(elements, env);
219+
instr.push(arrayInstr);
220+
221+
const block: BasicBlock = {
222+
kind: 'block',
223+
id: makeBlockId(0),
224+
instructions: instr,
225+
terminal: {
226+
id: makeInstructionId(0),
227+
kind: 'return',
228+
loc: GeneratedSource,
229+
value: arrayInstr.lvalue,
230+
},
231+
preds: new Set(),
232+
phis: new Set(),
233+
};
234+
235+
const fn: HIRFunction = {
236+
loc: GeneratedSource,
237+
id: null,
238+
fnType: 'Other',
239+
env,
240+
params: [obj],
241+
returnType: null,
242+
context: [],
243+
effects: null,
244+
body: {
245+
entry: block.id,
246+
blocks: new Map([[block.id, block]]),
247+
},
248+
generator: false,
249+
async: false,
250+
directives: [],
251+
};
252+
253+
reversePostorderBlocks(fn.body);
254+
removeUnreachableForUpdates(fn.body);
255+
removeDeadDoWhileStatements(fn.body);
256+
removeUnnecessaryTryCatch(fn.body);
257+
markInstructionIds(fn.body);
258+
mergeConsecutiveBlocks(fn);
259+
enterSSA(fn);
260+
inferTypes(fn);
261+
262+
const fnInstr: Instruction = {
263+
id: makeInstructionId(0),
264+
value: {
265+
kind: 'FunctionExpression',
266+
name: null,
267+
loweredFunc: {
268+
func: fn,
269+
dependencies: [],
270+
},
271+
type: 'ArrowFunctionExpression',
272+
loc: GeneratedSource,
273+
},
274+
lvalue: {
275+
kind: 'Identifier',
276+
identifier: makeTemporary(env.nextIdentifierId, GeneratedSource),
277+
effect: Effect.Unknown,
278+
reactive: false,
279+
loc: GeneratedSource,
280+
},
281+
loc: GeneratedSource,
282+
};
283+
return fnInstr;
284+
}
285+
286+
function emitArrayInstr(elements: Array<Place>, env: Environment): Instruction {
287+
const array: ArrayExpression = {
288+
kind: 'ArrayExpression',
289+
elements,
290+
loc: GeneratedSource,
291+
};
292+
const arrayLvalue: Place = {
293+
kind: 'Identifier',
294+
identifier: makeTemporary(env.nextIdentifierId, GeneratedSource),
295+
effect: Effect.Unknown,
296+
reactive: false,
297+
loc: GeneratedSource,
298+
};
299+
const arrayInstr: Instruction = {
300+
id: makeInstructionId(0),
301+
value: array,
302+
lvalue: arrayLvalue,
303+
loc: GeneratedSource,
304+
};
305+
return arrayInstr;
306+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @enableLowerContextAccess
6+
function App() {
7+
const {foo} = useContext(MyContext);
8+
const {bar} = useContext(MyContext);
9+
return <Bar foo={foo} bar={bar} />;
10+
}
11+
12+
```
13+
14+
## Code
15+
16+
```javascript
17+
import { c as _c } from "react/compiler-runtime"; // @enableLowerContextAccess
18+
function App() {
19+
const $ = _c(3);
20+
const { foo } = useContext(MyContext, _temp);
21+
const { bar } = useContext(MyContext, _temp2);
22+
let t0;
23+
if ($[0] !== foo || $[1] !== bar) {
24+
t0 = <Bar foo={foo} bar={bar} />;
25+
$[0] = foo;
26+
$[1] = bar;
27+
$[2] = t0;
28+
} else {
29+
t0 = $[2];
30+
}
31+
return t0;
32+
}
33+
function _temp2(c) {
34+
return [c.bar];
35+
}
36+
function _temp(c) {
37+
return [c.foo];
38+
}
39+
40+
```
41+
42+
### Eval output
43+
(kind: exception) Fixture not implemented
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// @enableLowerContextAccess
2+
function App() {
3+
const {foo} = useContext(MyContext);
4+
const {bar} = useContext(MyContext);
5+
return <Bar foo={foo} bar={bar} />;
6+
}

0 commit comments

Comments
 (0)