Skip to content

Commit 6654291

Browse files
committed
Update base for Update on "[compiler] Optimize emission in normal (non-value) blocks"
In #29863 I tried to find a clean way to share code for emitting instructions between value blocks and regular blocks. The catch is that value blocks have special meaning for their final instruction — that's the value of the block — so reordering can't change the last instruction. However, in finding a clean way to share code for these two categories of code, i also inadvertently reduced the effectiveness of the optimization. This PR updates to use different strategies for these two kinds of blocks: value blocks use the code from #29863 where we first emit all non-reorderable instructions in their original order, _then_ try to emit reorderable values. The reason this is suboptimal, though, is that we want to move instructions closer to their dependencies so that they can invalidate (merge) together. Emitting the reorderable values last prevents this. So for normal blocks, we now emit terminal operands first. This will invariably cause _some_ of the non-reorderable instructions to be emitted, but it will intersperse reoderable instructions in between, right after their dependencies. This maximizes our ability to merge scopes. I think the complexity cost of two strategies is worth the benefit, though i still need to double-check all the output changes. [ghstack-poisoned]
2 parents d289535 + f14d7f0 commit 6654291

File tree

31 files changed

+635
-246
lines changed

31 files changed

+635
-246
lines changed

.prettierrc.js

+6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ module.exports = {
1414
parser: 'flow',
1515
arrowParens: 'avoid',
1616
overrides: [
17+
{
18+
files: ['*.code-workspace'],
19+
options: {
20+
parser: 'json-stringify',
21+
},
22+
},
1723
{
1824
files: esNextPaths,
1925
options: {

compiler/packages/babel-plugin-react-compiler/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "babel-plugin-react-compiler",
3-
"version": "0.0.0-experimental-938cd9a-20240601",
3+
"version": "0.0.0-experimental-179941d-20240614",
44
"description": "Babel plugin for React Compiler.",
55
"main": "dist/index.js",
66
"license": "MIT",

compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -1668,6 +1668,15 @@ function lowerExpression(
16681668
const left = lowerExpressionToTemporary(builder, leftPath);
16691669
const right = lowerExpressionToTemporary(builder, expr.get("right"));
16701670
const operator = expr.node.operator;
1671+
if (operator === "|>") {
1672+
builder.errors.push({
1673+
reason: `(BuildHIR::lowerExpression) Pipe operator not supported`,
1674+
severity: ErrorSeverity.Todo,
1675+
loc: leftPath.node.loc ?? null,
1676+
suggestions: null,
1677+
});
1678+
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
1679+
}
16711680
return {
16721681
kind: "BinaryExpression",
16731682
operator,
@@ -1893,7 +1902,9 @@ function lowerExpression(
18931902
);
18941903
}
18951904

1896-
const operators: { [key: string]: t.BinaryExpression["operator"] } = {
1905+
const operators: {
1906+
[key: string]: Exclude<t.BinaryExpression["operator"], "|>">;
1907+
} = {
18971908
"+=": "+",
18981909
"-=": "-",
18991910
"/=": "/",
@@ -2307,6 +2318,20 @@ function lowerExpression(
23072318
});
23082319
return { kind: "UnsupportedNode", node: expr.node, loc: exprLoc };
23092320
}
2321+
} else if (expr.node.operator === "throw") {
2322+
builder.errors.push({
2323+
reason: `Throw expressions are not supported`,
2324+
severity: ErrorSeverity.InvalidJS,
2325+
loc: expr.node.loc ?? null,
2326+
suggestions: [
2327+
{
2328+
description: "Remove this line",
2329+
range: [expr.node.start!, expr.node.end!],
2330+
op: CompilerSuggestionOperation.Remove,
2331+
},
2332+
],
2333+
});
2334+
return { kind: "UnsupportedNode", node: expr.node, loc: exprLoc };
23102335
} else {
23112336
return {
23122337
kind: "UnaryExpression",

compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts

+23
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,18 @@ export type Hook = z.infer<typeof HookSchema>;
119119
const EnvironmentConfigSchema = z.object({
120120
customHooks: z.map(z.string(), HookSchema).optional().default(new Map()),
121121

122+
/**
123+
* A list of functions which the application compiles as macros, where
124+
* the compiler must ensure they are not compiled to rename the macro or separate the
125+
* "function" from its argument.
126+
*
127+
* For example, Meta has some APIs such as `featureflag("name-of-feature-flag")` which
128+
* are rewritten by a plugin. Assigning `featureflag` to a temporary would break the
129+
* plugin since it looks specifically for the name of the function being invoked, not
130+
* following aliases.
131+
*/
132+
customMacros: z.nullable(z.array(z.string())).default(null),
133+
122134
/**
123135
* Enable a check that resets the memoization cache when the source code of the file changes.
124136
* This is intended to support hot module reloading (HMR), where the same runtime component
@@ -416,6 +428,17 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
416428
continue;
417429
}
418430

431+
if (
432+
key === "enableChangeDetectionForDebugging" &&
433+
(val === undefined || val === "true")
434+
) {
435+
maybeConfig[key] = {
436+
source: "react-compiler-runtime",
437+
importSpecifierName: "$structuralCheck",
438+
};
439+
continue;
440+
}
441+
419442
if (typeof defaultConfig[key as keyof EnvironmentConfig] !== "boolean") {
420443
// skip parsing non-boolean properties
421444
continue;

compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -888,7 +888,7 @@ export type InstructionValue =
888888
| JSXText
889889
| {
890890
kind: "BinaryExpression";
891-
operator: t.BinaryExpression["operator"];
891+
operator: Exclude<t.BinaryExpression["operator"], "|>">;
892892
left: Place;
893893
right: Place;
894894
loc: SourceLocation;
@@ -903,7 +903,7 @@ export type InstructionValue =
903903
| MethodCall
904904
| {
905905
kind: "UnaryExpression";
906-
operator: t.UnaryExpression["operator"];
906+
operator: Exclude<t.UnaryExpression["operator"], "throw" | "delete">;
907907
value: Place;
908908
loc: SourceLocation;
909909
}

compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -856,7 +856,7 @@ export function mapTerminalSuccessors(
856856
const block = fn(terminal.block);
857857
const fallthrough = fn(terminal.fallthrough);
858858
return {
859-
kind: "scope",
859+
kind: terminal.kind,
860860
scope: terminal.scope,
861861
block,
862862
fallthrough,

compiler/packages/babel-plugin-react-compiler/src/Optimization/InstructionReordering.ts

-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
IdentifierId,
1515
Instruction,
1616
InstructionId,
17-
MutableRange,
1817
Place,
1918
isExpressionBlockKind,
2019
makeInstructionId,
@@ -26,7 +25,6 @@ import {
2625
eachInstructionValueLValue,
2726
eachInstructionValueOperand,
2827
eachTerminalOperand,
29-
terminalFallthrough,
3028
} from "../HIR/visitors";
3129
import { mayAllocate } from "../ReactiveScopes/InferReactiveScopeVariables";
3230
import { getOrInsertWith } from "../Utils/utils";

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import { Err, Ok, Result } from "../Utils/Result";
4343
import { GuardKind } from "../Utils/RuntimeDiagnosticConstants";
4444
import { assertExhaustive } from "../Utils/utils";
4545
import { buildReactiveFunction } from "./BuildReactiveFunction";
46-
import { SINGLE_CHILD_FBT_TAGS } from "./MemoizeFbtOperandsInSameScope";
46+
import { SINGLE_CHILD_FBT_TAGS } from "./MemoizeFbtAndMacroOperandsInSameScope";
4747
import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";
4848

4949
export const MEMO_CACHE_SENTINEL = "react.memo_cache_sentinel";

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MemoizeFbtOperandsInSameScope.ts compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MemoizeFbtAndMacroOperandsInSameScope.ts

+31-10
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,15 @@ import {
1414
} from "../HIR";
1515
import { eachReactiveValueOperand } from "./visitors";
1616

17-
/*
18-
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/).
17+
/**
18+
* This pass supports the
19+
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/)
20+
* as well as similar user-configurable macro-like APIs where it's important that
21+
* the name of the function not be changed, and it's literal arguments not be turned
22+
* into temporaries.
23+
*
24+
* ## FBT
25+
*
1926
* FBT provides the `<fbt>` JSX element and `fbt()` calls (which take params in the
2027
* form of `<fbt:param>` children or `fbt.param()` arguments, respectively). These
2128
* tags/functions have restrictions on what types of syntax may appear as props/children/
@@ -26,13 +33,22 @@ import { eachReactiveValueOperand } from "./visitors";
2633
* operands to fbt tags/calls have the same scope as the tag/call itself.
2734
*
2835
* Note that this still allows the props/arguments of `<fbt:param>`/`fbt.param()`
29-
* to be independently memoized
36+
* to be independently memoized.
37+
*
38+
* ## User-defined macro-like function
39+
*
40+
* Users can also specify their own functions to be treated similarly to fbt via the
41+
* `customMacros` environment configuration.
3042
*/
31-
export function memoizeFbtOperandsInSameScope(fn: HIRFunction): void {
43+
export function memoizeFbtAndMacroOperandsInSameScope(fn: HIRFunction): void {
44+
const fbtMacroTags = new Set([
45+
...FBT_TAGS,
46+
...(fn.env.config.customMacros ?? []),
47+
]);
3248
const fbtValues: Set<IdentifierId> = new Set();
3349
while (true) {
3450
let size = fbtValues.size;
35-
visit(fn, fbtValues);
51+
visit(fn, fbtMacroTags, fbtValues);
3652
if (size === fbtValues.size) {
3753
break;
3854
}
@@ -50,7 +66,11 @@ export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
5066
"fbs:param",
5167
]);
5268

53-
function visit(fn: HIRFunction, fbtValues: Set<IdentifierId>): void {
69+
function visit(
70+
fn: HIRFunction,
71+
fbtMacroTags: Set<string>,
72+
fbtValues: Set<IdentifierId>
73+
): void {
5474
for (const [, block] of fn.body.blocks) {
5575
for (const instruction of block.instructions) {
5676
const { lvalue, value } = instruction;
@@ -60,7 +80,7 @@ function visit(fn: HIRFunction, fbtValues: Set<IdentifierId>): void {
6080
if (
6181
value.kind === "Primitive" &&
6282
typeof value.value === "string" &&
63-
FBT_TAGS.has(value.value)
83+
fbtMacroTags.has(value.value)
6484
) {
6585
/*
6686
* We don't distinguish between tag names and strings, so record
@@ -69,7 +89,7 @@ function visit(fn: HIRFunction, fbtValues: Set<IdentifierId>): void {
6989
fbtValues.add(lvalue.identifier.id);
7090
} else if (
7191
value.kind === "LoadGlobal" &&
72-
FBT_TAGS.has(value.binding.name)
92+
fbtMacroTags.has(value.binding.name)
7393
) {
7494
// Record references to `fbt` as a global
7595
fbtValues.add(lvalue.identifier.id);
@@ -97,7 +117,7 @@ function visit(fn: HIRFunction, fbtValues: Set<IdentifierId>): void {
97117
fbtValues.add(operand.identifier.id);
98118
}
99119
} else if (
100-
isFbtJsxExpression(fbtValues, value) ||
120+
isFbtJsxExpression(fbtMacroTags, fbtValues, value) ||
101121
isFbtJsxChild(fbtValues, lvalue, value)
102122
) {
103123
const fbtScope = lvalue.identifier.scope;
@@ -169,14 +189,15 @@ function isFbtCallExpression(
169189
}
170190

171191
function isFbtJsxExpression(
192+
fbtMacroTags: Set<string>,
172193
fbtValues: Set<IdentifierId>,
173194
value: ReactiveValue
174195
): boolean {
175196
return (
176197
value.kind === "JsxExpression" &&
177198
((value.tag.kind === "Identifier" &&
178199
fbtValues.has(value.tag.identifier.id)) ||
179-
(value.tag.kind === "BuiltinTag" && FBT_TAGS.has(value.tag.name)))
200+
(value.tag.kind === "BuiltinTag" && fbtMacroTags.has(value.tag.name)))
180201
);
181202
}
182203

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export { extractScopeDeclarationsFromDestructuring } from "./ExtractScopeDeclara
1919
export { flattenReactiveLoops } from "./FlattenReactiveLoops";
2020
export { flattenScopesWithHooksOrUse } from "./FlattenScopesWithHooksOrUse";
2121
export { inferReactiveScopeVariables } from "./InferReactiveScopeVariables";
22-
export { memoizeFbtOperandsInSameScope } from "./MemoizeFbtOperandsInSameScope";
22+
export { memoizeFbtAndMacroOperandsInSameScope as memoizeFbtOperandsInSameScope } from "./MemoizeFbtAndMacroOperandsInSameScope";
2323
export { mergeOverlappingReactiveScopes } from "./MergeOverlappingReactiveScopes";
2424
export { mergeReactiveScopesThatInvalidateTogether } from "./MergeReactiveScopesThatInvalidateTogether";
2525
export { printReactiveFunction } from "./PrintReactiveFunction";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @compilationMode(infer) @enableAssumeHooksFollowRulesOfReact:false @customMacros(cx)
6+
import { identity } from "shared-runtime";
7+
8+
const DARK = "dark";
9+
10+
function Component() {
11+
const theme = useTheme();
12+
return (
13+
<div
14+
className={cx({
15+
"styles/light": true,
16+
"styles/dark": theme.getTheme() === DARK,
17+
})}
18+
/>
19+
);
20+
}
21+
22+
function cx(obj) {
23+
const classes = [];
24+
for (const [key, value] of Object.entries(obj)) {
25+
if (value) {
26+
classes.push(key);
27+
}
28+
}
29+
return classes.join(" ");
30+
}
31+
32+
function useTheme() {
33+
return {
34+
getTheme() {
35+
return DARK;
36+
},
37+
};
38+
}
39+
40+
export const FIXTURE_ENTRYPOINT = {
41+
fn: Component,
42+
params: [{}],
43+
};
44+
45+
```
46+
47+
## Code
48+
49+
```javascript
50+
import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer) @enableAssumeHooksFollowRulesOfReact:false @customMacros(cx)
51+
import { identity } from "shared-runtime";
52+
53+
const DARK = "dark";
54+
55+
function Component() {
56+
const $ = _c(2);
57+
const theme = useTheme();
58+
59+
const t0 = cx({
60+
"styles/light": true,
61+
"styles/dark": theme.getTheme() === DARK,
62+
});
63+
let t1;
64+
if ($[0] !== t0) {
65+
t1 = <div className={t0} />;
66+
$[0] = t0;
67+
$[1] = t1;
68+
} else {
69+
t1 = $[1];
70+
}
71+
return t1;
72+
}
73+
74+
function cx(obj) {
75+
const classes = [];
76+
for (const [key, value] of Object.entries(obj)) {
77+
if (value) {
78+
classes.push(key);
79+
}
80+
}
81+
return classes.join(" ");
82+
}
83+
84+
function useTheme() {
85+
return {
86+
getTheme() {
87+
return DARK;
88+
},
89+
};
90+
}
91+
92+
export const FIXTURE_ENTRYPOINT = {
93+
fn: Component,
94+
params: [{}],
95+
};
96+
97+
```
98+
99+
### Eval output
100+
(kind: ok) <div class="styles/light styles/dark"></div>

0 commit comments

Comments
 (0)