@samitouri / QOS-React / commits / d99f8bba2e

[compiler] Delete LoweredFunction.dependencies and hoisted instructions (#32096)

LoweredFunction dependencies were exclusively used for dependency extraction (in `propagateScopeDeps`). Now that we have a `propagateScopeDepsHIR` that recursively traverses into nested functions, we can delete `dependencies` and their associated synthetic `LoadLocal`/`PropertyLoad` instructions. [Internal snapshot diff](https://www.internalfb.com/phabricator/paste/view/P1716950202) for this change shows ~.2% of files changed. I [read through ~60 of the changed files](https://www.internalfb.com/phabricator/paste/view/P1733074307) - most changes are due to better outlining (due to better DCE) - a few changes in memo inference are due to changed ordering ``` // source arr.map(() => contextVar.inner); // previous instructions $0 = LoadLocal arr $1 = $0.map // Below instructions are synthetic $2 = LoadLocal contextVar $3 = $2.inner $4 = Function deps=$3 context=contextVar { ... } ``` - a few changes are effectively bugfixes (see `aliased-nested-scope-fn-expr`) --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32096). * #32099 * #32286 * #32104 * #32098 * #32097 * __->__ #32096

mofeiZ committed Feb 18, 2025 at 09:32 UTC d99f8bba2e07e3bb953f0821d4da5e341136fe5c
55 files changed +1123 -473
compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts
+12 -142
@@ -7,7 +7,6 @@
7
8 import {NodePath, Scope} from '@babel/traverse';
9 import * as t from '@babel/types';
10 -import {Expression} from '@babel/types';
10 import invariant from 'invariant';
11 import {
12 CompilerError,
@@ -75,7 +74,7 @@ export function lower(
74 parent: NodePath<t.Function> | null = null,
75 ): Result<HIRFunction, CompilerError> {
76 const builder = new HIRBuilder(env, parent ?? func, bindings, capturedRefs);
78 - const context: Array<Place> = [];
77 + const context: HIRFunction['context'] = [];
78
79 for (const ref of capturedRefs ?? []) {
80 context.push({
@@ -3378,7 +3377,7 @@ function lowerFunction(
3377 >,
3378 ): LoweredFunction | null {
3379 const componentScope: Scope = builder.parentFunction.scope;
3381 - const captured = gatherCapturedDeps(builder, expr, componentScope);
3380 + const capturedContext = gatherCapturedContext(expr, componentScope);
3381
3382 /*
3383 * TODO(gsn): In the future, we could only pass in the context identifiers
@@ -3392,7 +3391,7 @@ function lowerFunction(
3391 expr,
3392 builder.environment,
3393 builder.bindings,
3395 - [...builder.context, ...captured.identifiers],
3394 + [...builder.context, ...capturedContext],
3395 builder.parentFunction,
3396 );
3397 let loweredFunc: HIRFunction;
@@ -3405,7 +3404,6 @@ function lowerFunction(
3404 loweredFunc = lowering.unwrap();
3405 return {
3406 func: loweredFunc,
3408 - dependencies: captured.refs,
3407 };
3408 }
3409
@@ -4079,14 +4077,6 @@ function lowerAssignment(
4077 }
4078 }
4079
4082 -function isValidDependency(path: NodePath<t.MemberExpression>): boolean {
4083 - const parent: NodePath<t.Node> = path.parentPath;
4084 - return (
4085 - !path.node.computed &&
4086 - !(parent.isCallExpression() && parent.get('callee') === path)
4087 - );
4088 -}
4089 -
4080 function captureScopes({from, to}: {from: Scope; to: Scope}): Set<Scope> {
4081 let scopes: Set<Scope> = new Set();
4082 while (from) {
@@ -4101,8 +4091,7 @@ function captureScopes({from, to}: {from: Scope; to: Scope}): Set<Scope> {
4091 return scopes;
4092 }
4093
4104 -function gatherCapturedDeps(
4105 - builder: HIRBuilder,
4094 +function gatherCapturedContext(
4095 fn: NodePath<
4096 | t.FunctionExpression
4097 | t.ArrowFunctionExpression
@@ -4110,10 +4099,8 @@ function gatherCapturedDeps(
4099 | t.ObjectMethod
4100 >,
4101 componentScope: Scope,
4113 -): {identifiers: Array<t.Identifier>; refs: Array<Place>} {
4114 - const capturedIds: Map<t.Identifier, number> = new Map();
4115 - const capturedRefs: Set<Place> = new Set();
4116 - const seenPaths: Set<string> = new Set();
4102 +): Array<t.Identifier> {
4103 + const capturedIds = new Set<t.Identifier>();
4104
4105 /*
4106 * Capture all the scopes from the parent of this function up to and including
@@ -4124,33 +4111,11 @@ function gatherCapturedDeps(
4111 to: componentScope,
4112 });
4113
4127 - function addCapturedId(bindingIdentifier: t.Identifier): number {
4128 - if (!capturedIds.has(bindingIdentifier)) {
4129 - const index = capturedIds.size;
4130 - capturedIds.set(bindingIdentifier, index);
4131 - return index;
4132 - } else {
4133 - return capturedIds.get(bindingIdentifier)!;
4134 - }
4135 - }
4136 -
4114 function handleMaybeDependency(
4138 - path:
4139 - | NodePath<t.MemberExpression>
4140 - | NodePath<t.Identifier>
4141 - | NodePath<t.JSXOpeningElement>,
4115 + path: NodePath<t.Identifier> | NodePath<t.JSXOpeningElement>,
4116 ): void {
4117 // Base context variable to depend on
4118 let baseIdentifier: NodePath<t.Identifier> | NodePath<t.JSXIdentifier>;
4145 - /*
4146 - * Base expression to depend on, which (for now) may contain non side-effectful
4147 - * member expressions
4148 - */
4149 - let dependency:
4150 - | NodePath<t.MemberExpression>
4151 - | NodePath<t.JSXMemberExpression>
4152 - | NodePath<t.Identifier>
4153 - | NodePath<t.JSXIdentifier>;
4119 if (path.isJSXOpeningElement()) {
4120 const name = path.get('name');
4121 if (!(name.isJSXMemberExpression() || name.isJSXIdentifier())) {
@@ -4166,115 +4131,20 @@ function gatherCapturedDeps(
4131 'Invalid logic in gatherCapturedDeps',
4132 );
4133 baseIdentifier = current;
4169 -
4170 - /*
4171 - * Get the expression to depend on, which may involve PropertyLoads
4172 - * for member expressions
4173 - */
4174 - let currentDep:
4175 - | NodePath<t.JSXMemberExpression>
4176 - | NodePath<t.Identifier>
4177 - | NodePath<t.JSXIdentifier> = baseIdentifier;
4178 -
4179 - while (true) {
4180 - const nextDep: null | NodePath<t.Node> = currentDep.parentPath;
4181 - if (nextDep && nextDep.isJSXMemberExpression()) {
4182 - currentDep = nextDep;
4183 - } else {
4184 - break;
4185 - }
4186 - }
4187 - dependency = currentDep;
4188 - } else if (path.isMemberExpression()) {
4189 - // Calculate baseIdentifier
4190 - let currentId: NodePath<Expression> = path;
4191 - while (currentId.isMemberExpression()) {
4192 - currentId = currentId.get('object');
4193 - }
4194 - if (!currentId.isIdentifier()) {
4195 - return;
4196 - }
4197 - baseIdentifier = currentId;
4198 -
4199 - /*
4200 - * Get the expression to depend on, which may involve PropertyLoads
4201 - * for member expressions
4202 - */
4203 - let currentDep:
4204 - | NodePath<t.MemberExpression>
4205 - | NodePath<t.Identifier>
4206 - | NodePath<t.JSXIdentifier> = baseIdentifier;
4207 -
4208 - while (true) {
4209 - const nextDep: null | NodePath<t.Node> = currentDep.parentPath;
4210 - if (
4211 - nextDep &&
4212 - nextDep.isMemberExpression() &&
4213 - isValidDependency(nextDep)
4214 - ) {
4215 - currentDep = nextDep;
4216 - } else {
4217 - break;
4218 - }
4219 - }
4220 -
4221 - dependency = currentDep;
4134 } else {
4135 baseIdentifier = path;
4224 - dependency = path;
4136 }
4137
4138 /*
4139 * Skip dependency path, as we already tried to recursively add it (+ all subexpressions)
4140 * as a dependency.
4141 */
4231 - dependency.skip();
4142 + path.skip();
4143
4144 // Add the base identifier binding as a dependency.
4145 const binding = baseIdentifier.scope.getBinding(baseIdentifier.node.name);
4235 - if (binding === undefined || !pureScopes.has(binding.scope)) {
4236 - return;
4237 - }
4238 - const idKey = String(addCapturedId(binding.identifier));
4239 -
4240 - // Add the expression (potentially a memberexpr path) as a dependency.
4241 - let exprKey = idKey;
4242 - if (dependency.isMemberExpression()) {
4243 - let pathTokens = [];
4244 - let current: NodePath<Expression> = dependency;
4245 - while (current.isMemberExpression()) {
4246 - const property = current.get('property') as NodePath<t.Identifier>;
4247 - pathTokens.push(property.node.name);
4248 - current = current.get('object');
4249 - }
4250 -
4251 - exprKey += '.' + pathTokens.reverse().join('.');
4252 - } else if (dependency.isJSXMemberExpression()) {
4253 - let pathTokens = [];
4254 - let current: NodePath<t.JSXMemberExpression | t.JSXIdentifier> =
4255 - dependency;
4256 - while (current.isJSXMemberExpression()) {
4257 - const property = current.get('property');
4258 - pathTokens.push(property.node.name);
4259 - current = current.get('object');
4260 - }
4261 - }
4262 -
4263 - if (!seenPaths.has(exprKey)) {
4264 - let loweredDep: Place;
4265 - if (dependency.isJSXIdentifier()) {
4266 - loweredDep = lowerValueToTemporary(builder, {
4267 - kind: 'LoadLocal',
4268 - place: lowerIdentifier(builder, dependency),
4269 - loc: path.node.loc ?? GeneratedSource,
4270 - });
4271 - } else if (dependency.isJSXMemberExpression()) {
4272 - loweredDep = lowerJsxMemberExpression(builder, dependency);
4273 - } else {
4274 - loweredDep = lowerExpressionToTemporary(builder, dependency);
4275 - }
4276 - capturedRefs.add(loweredDep);
4277 - seenPaths.add(exprKey);
4146 + if (binding !== undefined && pureScopes.has(binding.scope)) {
4147 + capturedIds.add(binding.identifier);
4148 }
4149 }
4150
@@ -4305,13 +4175,13 @@ function gatherCapturedDeps(
4175 return;
4176 } else if (path.isJSXElement()) {
4177 handleMaybeDependency(path.get('openingElement'));
4308 - } else if (path.isMemberExpression() || path.isIdentifier()) {
4178 + } else if (path.isIdentifier()) {
4179 handleMaybeDependency(path);
4180 }
4181 },
4182 });
4183
4314 - return {identifiers: [...capturedIds.keys()], refs: [...capturedRefs]};
4184 + return [...capturedIds.keys()];
4185 }
4186
4187 function notNull<T>(value: T | null): value is T {
compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts
+1 -36
@@ -131,15 +131,7 @@ function collectHoistablePropertyLoadsImpl(
131 fn: HIRFunction,
132 context: CollectHoistablePropertyLoadsContext,
133 ): ReadonlyMap<BlockId, BlockInfo> {
134 - const functionExpressionLoads = collectFunctionExpressionFakeLoads(fn);
135 - const actuallyEvaluatedTemporaries = new Map(
136 - [...context.temporaries].filter(([id]) => !functionExpressionLoads.has(id)),
137 - );
138 -
139 - const nodes = collectNonNullsInBlocks(fn, {
140 - ...context,
141 - temporaries: actuallyEvaluatedTemporaries,
142 - });
134 + const nodes = collectNonNullsInBlocks(fn, context);
135 propagateNonNull(fn, nodes, context.registry);
136
137 if (DEBUG_PRINT) {
@@ -598,30 +590,3 @@ function reduceMaybeOptionalChains(
590 }
591 } while (changed);
592 }
601 -
602 -function collectFunctionExpressionFakeLoads(
603 - fn: HIRFunction,
604 -): Set<IdentifierId> {
605 - const sources = new Map<IdentifierId, IdentifierId>();
606 - const functionExpressionReferences = new Set<IdentifierId>();
607 -
608 - for (const [_, block] of fn.body.blocks) {
609 - for (const {lvalue, value} of block.instructions) {
610 - if (
611 - value.kind === 'FunctionExpression' ||
612 - value.kind === 'ObjectMethod'
613 - ) {
614 - for (const reference of value.loweredFunc.dependencies) {
615 - let curr: IdentifierId | undefined = reference.identifier.id;
616 - while (curr != null) {
617 - functionExpressionReferences.add(curr);
618 - curr = sources.get(curr);
619 - }
620 - }
621 - } else if (value.kind === 'PropertyLoad') {
622 - sources.set(lvalue.identifier.id, value.object.identifier.id);
623 - }
624 - }
625 - }
626 - return functionExpressionReferences;
627 -}
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
-2
@@ -245,8 +245,6 @@ const EnvironmentConfigSchema = z.object({
245 */
246 enableUseTypeAnnotations: z.boolean().default(false),
247
248 - enableFunctionDependencyRewrite: z.boolean().default(true),
249 -
248 /**
249 * Enables inference of optional dependency chains. Without this flag
250 * a property chain such as `props?.items?.foo` will infer as a dep on
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
-1
@@ -722,7 +722,6 @@ export type ObjectProperty = {
722 };
723
724 export type LoweredFunction = {
725 - dependencies: Array<Place>;
725 func: HIRFunction;
726 };
727
compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts
+16 -1
@@ -148,6 +148,14 @@ function collectScopeInfo(fn: HIRFunction): ScopeInfo {
148 const scope = place.identifier.scope;
149 if (scope != null) {
150 placeScopes.set(place, scope);
151 + /**
152 + * Record both mutating and non-mutating scopes to merge scopes with
153 + * still-mutating values with inner scopes that alias those values
154 + * (see `nonmutating-capture-in-unsplittable-memo-block`)
155 + *
156 + * Note that this isn't perfect, as it also leads to merging of mutating
157 + * scopes with JSX single-instruction scopes (see `mutation-within-jsx`)
158 + */
159 if (scope.range.start !== scope.range.end) {
160 getOrInsertDefault(scopeStarts, scope.range.start, new Set()).add(
161 scope,
@@ -254,7 +262,7 @@ function visitPlace(
262 * of the stack to the mutated outer scope.
263 */
264 const placeScope = getPlaceScope(id, place);
257 - if (placeScope != null && isMutable({id} as any, place)) {
265 + if (placeScope != null && isMutable({id}, place)) {
266 const placeScopeIdx = activeScopes.indexOf(placeScope);
267 if (placeScopeIdx !== -1 && placeScopeIdx !== activeScopes.length - 1) {
268 joined.union([placeScope, ...activeScopes.slice(placeScopeIdx + 1)]);
@@ -275,6 +283,13 @@ function getOverlappingReactiveScopes(
283 for (const instr of block.instructions) {
284 visitInstructionId(instr.id, context, state);
285 for (const place of eachInstructionOperand(instr)) {
286 + if (
287 + (instr.value.kind === 'FunctionExpression' ||
288 + instr.value.kind === 'ObjectMethod') &&
289 + place.identifier.type.kind === 'Primitive'
290 + ) {
291 + continue;
292 + }
293 visitPlace(instr.id, place, state);
294 }
295 for (const place of eachInstructionLValue(instr)) {
compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts
+1 -4
@@ -538,9 +538,6 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
538 .split('\n')
539 .map(line => ` ${line}`)
540 .join('\n');
541 - const deps = instrValue.loweredFunc.dependencies
542 - .map(dep => printPlace(dep))
543 - .join(',');
541 const context = instrValue.loweredFunc.func.context
542 .map(dep => printPlace(dep))
543 .join(',');
@@ -557,7 +554,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
554 })
555 .join(', ') ?? '';
556 const type = printType(instrValue.loweredFunc.func.returnType).trim();
560 - value = `${kind} ${name} @deps[${deps}] @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`;
557 + value = `${kind} ${name} @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`;
558 break;
559 }
560 case 'TaggedTemplateExpression': {
compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts
+2 -3
@@ -738,9 +738,8 @@ function collectDependencies(
738 }
739 for (const instr of block.instructions) {
740 if (
741 - fn.env.config.enableFunctionDependencyRewrite &&
742 - (instr.value.kind === 'FunctionExpression' ||
743 - instr.value.kind === 'ObjectMethod')
741 + instr.value.kind === 'FunctionExpression' ||
742 + instr.value.kind === 'ObjectMethod'
743 ) {
744 context.declare(instr.lvalue.identifier, {
745 id: instr.id,
compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts
+4 -3
@@ -193,7 +193,7 @@ export function* eachInstructionValueOperand(
193 }
194 case 'ObjectMethod':
195 case 'FunctionExpression': {
196 - yield* instrValue.loweredFunc.dependencies;
196 + yield* instrValue.loweredFunc.func.context;
197 break;
198 }
199 case 'TaggedTemplateExpression': {
@@ -517,8 +517,9 @@ export function mapInstructionValueOperands(
517 }
518 case 'ObjectMethod':
519 case 'FunctionExpression': {
520 - instrValue.loweredFunc.dependencies =
521 - instrValue.loweredFunc.dependencies.map(d => fn(d));
520 + instrValue.loweredFunc.func.context =
521 + instrValue.loweredFunc.func.context.map(d => fn(d));
522 +
523 break;
524 }
525 case 'TaggedTemplateExpression': {
compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts
+31 -115
@@ -10,9 +10,8 @@ import {
10 Effect,
11 HIRFunction,
12 Identifier,
13 - IdentifierName,
13 + IdentifierId,
14 LoweredFunction,
15 - Place,
15 isRefOrRefValue,
16 makeInstructionId,
17 } from '../HIR';
@@ -23,78 +22,39 @@ import {inferMutableContextVariables} from './InferMutableContextVariables';
22 import {inferMutableRanges} from './InferMutableRanges';
23 import inferReferenceEffects from './InferReferenceEffects';
24
26 -type Dependency = {
27 - identifier: Identifier;
28 - path: Array<string>;
29 -};
30 -
25 // Helper class to track indirections such as LoadLocal and PropertyLoad.
26 export class IdentifierState {
33 - properties: Map<Identifier, Dependency> = new Map();
27 + properties: Map<IdentifierId, Identifier> = new Map();
28
29 resolve(identifier: Identifier): Identifier {
36 - const resolved = this.properties.get(identifier);
30 + const resolved = this.properties.get(identifier.id);
31 if (resolved !== undefined) {
38 - return resolved.identifier;
32 + return resolved;
33 }
34 return identifier;
35 }
36
43 - declareProperty(lvalue: Place, object: Place, property: string): void {
44 - const objectDependency = this.properties.get(object.identifier);
45 - let nextDependency: Dependency;
46 - if (objectDependency === undefined) {
47 - nextDependency = {identifier: object.identifier, path: [property]};
48 - } else {
49 - nextDependency = {
50 - identifier: objectDependency.identifier,
51 - path: [...objectDependency.path, property],
52 - };
53 - }
54 - this.properties.set(lvalue.identifier, nextDependency);
55 - }
56 -
57 - declareTemporary(lvalue: Place, value: Place): void {
58 - const resolved: Dependency = this.properties.get(value.identifier) ?? {
59 - identifier: value.identifier,
60 - path: [],
61 - };
62 - this.properties.set(lvalue.identifier, resolved);
37 + alias(lvalue: Identifier, value: Identifier): void {
38 + this.properties.set(lvalue.id, this.properties.get(value.id) ?? value);
39 }
40 }
41
42 export default function analyseFunctions(func: HIRFunction): void {
67 - const state = new IdentifierState();
68 -
43 for (const [_, block] of func.body.blocks) {
44 for (const instr of block.instructions) {
45 switch (instr.value.kind) {
46 case 'ObjectMethod':
47 case 'FunctionExpression': {
48 lower(instr.value.loweredFunc.func);
75 - infer(instr.value.loweredFunc, state, func.context);
76 - break;
77 - }
78 - case 'PropertyLoad': {
79 - state.declareProperty(
80 - instr.lvalue,
81 - instr.value.object,
82 - instr.value.property,
83 - );
84 - break;
85 - }
86 - case 'ComputedLoad': {
87 - /*
88 - * The path is set to an empty string as the path doesn't really
89 - * matter for a computed load.
49 + infer(instr.value.loweredFunc);
50 +
51 + /**
52 + * Reset mutable range for outer inferReferenceEffects
53 */
91 - state.declareProperty(instr.lvalue, instr.value.object, '');
92 - break;
93 - }
94 - case 'LoadLocal':
95 - case 'LoadContext': {
96 - if (instr.lvalue.identifier.name === null) {
97 - state.declareTemporary(instr.lvalue, instr.value.place);
54 + for (const operand of instr.value.loweredFunc.func.context) {
55 + operand.identifier.mutableRange.start = makeInstructionId(0);
56 + operand.identifier.mutableRange.end = makeInstructionId(0);
57 + operand.identifier.scope = null;
58 }
59 break;
60 }
@@ -110,7 +70,6 @@ function lower(func: HIRFunction): void {
70 inferMutableRanges(func);
71 rewriteInstructionKindsBasedOnReassignment(func);
72 inferReactiveScopeVariables(func);
113 - inferMutableContextVariables(func);
73 func.env.logger?.debugLogIRs?.({
74 kind: 'hir',
75 name: 'AnalyseFunction (inner)',
@@ -118,32 +77,16 @@ function lower(func: HIRFunction): void {
77 });
78 }
79
121 -function infer(
122 - loweredFunc: LoweredFunction,
123 - state: IdentifierState,
124 - context: Array<Place>,
125 -): void {
126 - const mutations = new Map<string, Effect>();
80 +function infer(loweredFunc: LoweredFunction): void {
81 + const knownMutated = inferMutableContextVariables(loweredFunc.func);
82 for (const operand of loweredFunc.func.context) {
128 - if (
129 - isMutatedOrReassigned(operand.identifier) &&
130 - operand.identifier.name !== null
131 - ) {
132 - mutations.set(operand.identifier.name.value, operand.effect);
133 - }
134 - }
135 -
136 - for (const dep of loweredFunc.dependencies) {
137 - let name: IdentifierName | null = null;
138 -
139 - if (state.properties.has(dep.identifier)) {
140 - const receiver = state.properties.get(dep.identifier)!;
141 - name = receiver.identifier.name;
142 - } else {
143 - name = dep.identifier.name;
144 - }
145 -
146 - if (isRefOrRefValue(dep.identifier)) {
83 + const identifier = operand.identifier;
84 + CompilerError.invariant(operand.effect === Effect.Unknown, {
85 + reason:
86 + '[AnalyseFunctions] Expected Function context effects to not have been set',
87 + loc: operand.loc,
88 + });
89 + if (isRefOrRefValue(identifier)) {
90 /*
91 * TODO: this is a hack to ensure we treat functions which reference refs
92 * as having a capture and therefore being considered mutable. this ensures
@@ -151,43 +94,16 @@ function infer(
94 * could be called, and allows us to help ensure it isn't called during
95 * render
96 */
154 - dep.effect = Effect.Capture;
155 - } else if (name !== null) {
156 - const effect = mutations.get(name.value);
157 - if (effect !== undefined) {
158 - dep.effect = effect === Effect.Unknown ? Effect.Capture : effect;
159 - }
160 - }
161 - }
162 -
163 - /*
164 - * This could potentially add duplicate deps to mutatedDeps in the case of
165 - * mutating a context ref in the child function and in this parent function.
166 - * It might be useful to dedupe this.
167 - *
168 - * In practice this never really matters because the Component function has no
169 - * context refs, so it will never have duplicate deps.
170 - */
171 - for (const place of context) {
172 - CompilerError.invariant(place.identifier.name !== null, {
173 - reason: 'context refs should always have a name',
174 - description: null,
175 - loc: place.loc,
176 - suggestions: null,
177 - });
178 -
179 - const effect = mutations.get(place.identifier.name.value);
180 - if (effect !== undefined) {
181 - place.effect = effect === Effect.Unknown ? Effect.Capture : effect;
182 - loweredFunc.dependencies.push(place);
97 + operand.effect = Effect.Capture;
98 + } else if (knownMutated.has(operand)) {
99 + operand.effect = Effect.Mutate;
100 + } else if (isMutatedOrReassigned(identifier)) {
101 + // Note that this also reflects if identifier is ConditionallyMutated
102 + operand.effect = Effect.Capture;
103 + } else {
104 + operand.effect = Effect.Read;
105 }
106 }
185 -
186 - for (const operand of loweredFunc.func.context) {
187 - operand.identifier.mutableRange.start = makeInstructionId(0);
188 - operand.identifier.mutableRange.end = makeInstructionId(0);
189 - operand.identifier.scope = null;
190 - }
107 }
108
109 function isMutatedOrReassigned(id: Identifier): boolean {
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableContextVariables.ts
+7 -16
@@ -55,32 +55,21 @@ import {IdentifierState} from './AnalyseFunctions';
55 * fn();
56 * ```
57 */
58 -export function inferMutableContextVariables(fn: HIRFunction): void {
58 +export function inferMutableContextVariables(fn: HIRFunction): Set<Place> {
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 - }
64 + case 'PropertyLoad':
65 case 'ComputedLoad': {
73 - /*
74 - * The path is set to an empty string as the path doesn't really
75 - * matter for a computed load.
76 - */
77 - state.declareProperty(instr.lvalue, instr.value.object, '');
66 + state.alias(instr.lvalue.identifier, instr.value.object.identifier);
67 break;
68 }
69 case 'LoadLocal':
70 case 'LoadContext': {
71 if (instr.lvalue.identifier.name === null) {
83 - state.declareTemporary(instr.lvalue, instr.value.place);
72 + state.alias(instr.lvalue.identifier, instr.value.place.identifier);
73 }
74 break;
75 }
@@ -95,11 +84,13 @@ export function inferMutableContextVariables(fn: HIRFunction): void {
84 visitOperand(state, knownMutatedIdentifiers, operand);
85 }
86 }
87 + const results = new Set<Place>();
88 for (const operand of fn.context) {
89 if (knownMutatedIdentifiers.has(operand.identifier)) {
100 - operand.effect = Effect.Mutate;
90 + results.add(operand);
91 }
92 }
93 + return results;
94 }
95
96 function visitOperand(
compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts
+25 -23
@@ -390,29 +390,31 @@ class InferenceState {
390
391 freezeValues(values: Set<InstructionValue>, reason: Set<ValueReason>): void {
392 for (const value of values) {
393 + if (value.kind === 'DeclareContext') {
394 + /**
395 + * Avoid freezing hoisted context declarations
396 + * function Component() {
397 + * const cb = useBar(() => foo(2)); // produces a hoisted context declaration
398 + * const foo = useFoo(); // reassigns to the context variable
399 + * return <Foo cb={cb} />;
400 + * }
401 + */
402 + continue;
403 + }
404 this.#values.set(value, {
405 kind: ValueKind.Frozen,
406 reason,
407 context: new Set(),
408 });
398 - if (value.kind === 'FunctionExpression') {
399 - if (
400 - this.#env.config.enablePreserveExistingMemoizationGuarantees ||
401 - this.#env.config.enableTransitivelyFreezeFunctionExpressions
402 - ) {
403 - if (value.kind === 'FunctionExpression') {
404 - /*
405 - * We want to freeze the captured values, not mark the operands
406 - * themselves as frozen. There could be mutations that occur
407 - * before the freeze we are processing, and it would be invalid
408 - * to overwrite those mutations as a freeze.
409 - */
410 - for (const operand of eachInstructionValueOperand(value)) {
411 - const operandValues = this.#variables.get(operand.identifier.id);
412 - if (operandValues !== undefined) {
413 - this.freezeValues(operandValues, reason);
414 - }
415 - }
409 + if (
410 + value.kind === 'FunctionExpression' &&
411 + (this.#env.config.enablePreserveExistingMemoizationGuarantees ||
412 + this.#env.config.enableTransitivelyFreezeFunctionExpressions)
413 + ) {
414 + for (const operand of value.loweredFunc.func.context) {
415 + const operandValues = this.#variables.get(operand.identifier.id);
416 + if (operandValues !== undefined) {
417 + this.freezeValues(operandValues, reason);
418 }
419 }
420 }
@@ -1143,17 +1145,17 @@ function inferBlock(
1145 case 'ObjectMethod':
1146 case 'FunctionExpression': {
1147 let hasMutableOperand = false;
1146 - const mutableOperands: Array<Place> = [];
1148 for (const operand of eachInstructionOperand(instr)) {
1149 + CompilerError.invariant(operand.effect !== Effect.Unknown, {
1150 + reason: 'Expected fn effects to be populated',
1151 + loc: operand.loc,
1152 + });
1153 state.referenceAndRecordEffects(
1154 freezeActions,
1155 operand,
1151 - operand.effect === Effect.Unknown ? Effect.Read : operand.effect,
1156 + operand.effect,
1157 ValueReason.Other,
1158 );
1154 - if (isMutableEffect(operand.effect, operand.loc)) {
1155 - mutableOperands.push(operand);
1156 - }
1159 hasMutableOperand ||= isMutableEffect(operand.effect, operand.loc);
1160 }
1161 /*
compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts
-1
@@ -270,7 +270,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
270 name: null,
271 loweredFunc: {
272 func: fn,
273 - dependencies: [],
273 },
274 type: 'ArrowFunctionExpression',
275 loc: GeneratedSource,
compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineFunctions.ts
-1
@@ -24,7 +24,6 @@ export function outlineFunctions(
24 }
25 if (
26 value.kind === 'FunctionExpression' &&
27 - value.loweredFunc.dependencies.length === 0 &&
27 value.loweredFunc.func.context.length === 0 &&
28 // TODO: handle outlining named functions
29 value.loweredFunc.func.id === null &&
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts
+8
@@ -379,6 +379,14 @@ export function findDisjointMutableValues(
379 */
380 operand.identifier.mutableRange.start > 0
381 ) {
382 + if (
383 + instr.value.kind === 'FunctionExpression' ||
384 + instr.value.kind === 'ObjectMethod'
385 + ) {
386 + if (operand.identifier.type.kind === 'Primitive') {
387 + continue;
388 + }
389 + }
390 operands.push(operand.identifier);
391 }
392 }
compiler/packages/babel-plugin-react-compiler/src/SSA/EliminateRedundantPhi.ts
+19
@@ -13,6 +13,8 @@ import {
13 eachTerminalOperand,
14 } from '../HIR/visitors';
15
16 +const DEBUG = false;
17 +
18 /*
19 * Pass to eliminate redundant phi nodes:
20 * - all operands are the same identifier, ie `x2 = phi(x1, x1, x1)`.
@@ -141,6 +143,23 @@ export function eliminateRedundantPhi(
143 * have already propagated forwards since we visit in reverse postorder.
144 */
145 } while (rewrites.size > size && hasBackEdge);
146 +
147 + if (DEBUG) {
148 + for (const [, block] of ir.blocks) {
149 + for (const phi of block.phis) {
150 + CompilerError.invariant(!rewrites.has(phi.place.identifier), {
151 + reason: '[EliminateRedundantPhis]: rewrite not complete',
152 + loc: phi.place.loc,
153 + });
154 + for (const [, operand] of phi.operands) {
155 + CompilerError.invariant(!rewrites.has(operand.identifier), {
156 + reason: '[EliminateRedundantPhis]: rewrite not complete',
157 + loc: phi.place.loc,
158 + });
159 + }
160 + }
161 + }
162 + }
163 }
164
165 function rewritePlace(
compiler/packages/babel-plugin-react-compiler/src/SSA/EnterSSA.ts
-3
@@ -301,9 +301,6 @@ function enterSSAImpl(
301 entry.preds.add(blockId);
302 builder.defineFunction(loweredFunc);
303 builder.enter(() => {
304 - loweredFunc.context = loweredFunc.context.map(p =>
305 - builder.getPlace(p),
306 - );
304 loweredFunc.params = loweredFunc.params.map(param => {
305 if (param.kind === 'Identifier') {
306 return builder.definePlace(param);
compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts
+6 -47
@@ -319,51 +319,6 @@ function visitFunctionExpressionAndPropagateFireDependencies(
319 replaceFireFunctions(fnExpr.loweredFunc.func, context),
320 );
321
322 - /*
323 - * Make a mapping from each dependency to the corresponding LoadLocal for it so that
324 - * we can replace the loaded place with the generated fire function binding
325 - */
326 - const loadLocalsToDepLoads = new Map<IdentifierId, LoadLocal>();
327 - for (const dep of fnExpr.loweredFunc.dependencies) {
328 - const loadLocal = context.getLoadLocalInstr(dep.identifier.id);
329 - if (loadLocal != null) {
330 - loadLocalsToDepLoads.set(loadLocal.place.identifier.id, loadLocal);
331 - }
332 - }
333 -
334 - const replacedCallees = new Map<IdentifierId, Place>();
335 - for (const [
336 - calleeIdentifierId,
337 - loadedFireFunctionBindingPlace,
338 - ] of calleesCapturedByFnExpression.entries()) {
339 - /*
340 - * Given the ids of captured fire callees, look at the deps for loads of those identifiers
341 - * and replace them with the new fire function binding
342 - */
343 - const loadLocal = loadLocalsToDepLoads.get(calleeIdentifierId);
344 - if (loadLocal == null) {
345 - context.pushError({
346 - loc: fnExpr.loc,
347 - description: null,
348 - severity: ErrorSeverity.Invariant,
349 - reason:
350 - '[InsertFire] No loadLocal found for fire call argument for lambda',
351 - suggestions: null,
352 - });
353 - continue;
354 - }
355 -
356 - const oldPlaceId = loadLocal.place.identifier.id;
357 - loadLocal.place = {
358 - ...loadedFireFunctionBindingPlace.fireFunctionBinding,
359 - };
360 -
361 - replacedCallees.set(
362 - oldPlaceId,
363 - loadedFireFunctionBindingPlace.fireFunctionBinding,
364 - );
365 - }
366 -
322 // For each replaced callee, update the context of the function expression to track it
323 for (
324 let contextIdx = 0;
@@ -371,9 +326,13 @@ function visitFunctionExpressionAndPropagateFireDependencies(
326 contextIdx++
327 ) {
328 const contextItem = fnExpr.loweredFunc.func.context[contextIdx];
374 - const replacedCallee = replacedCallees.get(contextItem.identifier.id);
329 + const replacedCallee = calleesCapturedByFnExpression.get(
330 + contextItem.identifier.id,
331 + );
332 if (replacedCallee != null) {
376 - fnExpr.loweredFunc.func.context[contextIdx] = replacedCallee;
333 + fnExpr.loweredFunc.func.context[contextIdx] = {
334 + ...replacedCallee.fireFunctionBinding,
335 + };
336 }
337 }
338
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-fn-expr.expect.md new
+120
@@ -0,0 +1,120 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableTransitivelyFreezeFunctionExpressions:false
6 +import {
7 + Stringify,
8 + mutate,
9 + identity,
10 + setPropertyByKey,
11 + shallowCopy,
12 +} from 'shared-runtime';
13 +/**
14 + * Function expression version of `aliased-nested-scope-truncated-dep`.
15 + *
16 + * In this fixture, the output would be invalid if propagateScopeDeps did not
17 + * avoid adding MemberExpression dependencies which would other evaluate during
18 + * the mutable ranges of their base objects.
19 + * This is different from `aliased-nested-scope-truncated-dep` which *does*
20 + * produce correct output regardless of MemberExpression dependency truncation.
21 + *
22 + * Note while other expressions evaluate inline, function expressions *always*
23 + * represent deferred evaluation. This means that
24 + * (1) it's always safe to reorder function expression creation until its
25 + * earliest potential invocation
26 + * (2) it's invalid to eagerly evaluate function expression dependencies during
27 + * their respective mutable ranges.
28 + */
29 +
30 +function Component({prop}) {
31 + let obj = shallowCopy(prop);
32 +
33 + const aliasedObj = identity(obj);
34 +
35 + // When `obj` is mutable (either directly or through aliases), taking a
36 + // dependency on `obj.id` is invalid as it may change before getId() is invoked
37 + const getId = () => obj.id;
38 +
39 + mutate(aliasedObj);
40 + setPropertyByKey(aliasedObj, 'id', prop.id + 1);
41 +
42 + // Calling getId() should return prop.id + 1, not the prev
43 + return <Stringify getId={getId} shouldInvokeFns={true} />;
44 +}
45 +
46 +export const FIXTURE_ENTRYPOINT = {
47 + fn: Component,
48 + params: [{prop: {id: 1}}],
49 + sequentialRenders: [{prop: {id: 1}}, {prop: {id: 1}}, {prop: {id: 2}}],
50 +};
51 +
52 +```
53 +
54 +## Code
55 +
56 +```javascript
57 +import { c as _c } from "react/compiler-runtime"; // @enableTransitivelyFreezeFunctionExpressions:false
58 +import {
59 + Stringify,
60 + mutate,
61 + identity,
62 + setPropertyByKey,
63 + shallowCopy,
64 +} from "shared-runtime";
65 +/**
66 + * Function expression version of `aliased-nested-scope-truncated-dep`.
67 + *
68 + * In this fixture, the output would be invalid if propagateScopeDeps did not
69 + * avoid adding MemberExpression dependencies which would other evaluate during
70 + * the mutable ranges of their base objects.
71 + * This is different from `aliased-nested-scope-truncated-dep` which *does*
72 + * produce correct output regardless of MemberExpression dependency truncation.
73 + *
74 + * Note while other expressions evaluate inline, function expressions *always*
75 + * represent deferred evaluation. This means that
76 + * (1) it's always safe to reorder function expression creation until its
77 + * earliest potential invocation
78 + * (2) it's invalid to eagerly evaluate function expression dependencies during
79 + * their respective mutable ranges.
80 + */
81 +
82 +function Component(t0) {
83 + const $ = _c(2);
84 + const { prop } = t0;
85 + let t1;
86 + if ($[0] !== prop) {
87 + const obj = shallowCopy(prop);
88 +
89 + const aliasedObj = identity(obj);
90 +
91 + const getId = () => obj.id;
92 +
93 + mutate(aliasedObj);
94 + setPropertyByKey(aliasedObj, "id", prop.id + 1);
95 +
96 + t1 = <Stringify getId={getId} shouldInvokeFns={true} />;
97 + $[0] = prop;
98 + $[1] = t1;
99 + } else {
100 + t1 = $[1];
101 + }
102 + return t1;
103 +}
104 +
105 +export const FIXTURE_ENTRYPOINT = {
106 + fn: Component,
107 + params: [{ prop: { id: 1 } }],
108 + sequentialRenders: [
109 + { prop: { id: 1 } },
110 + { prop: { id: 1 } },
111 + { prop: { id: 2 } },
112 + ],
113 +};
114 +
115 +```
116 +
117 +### Eval output
118 +(kind: ok) <div>{"getId":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
119 +<div>{"getId":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
120 +<div>{"getId":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-fn-expr.tsx new
+46
@@ -0,0 +1,46 @@
1 +// @enableTransitivelyFreezeFunctionExpressions:false
2 +import {
3 + Stringify,
4 + mutate,
5 + identity,
6 + setPropertyByKey,
7 + shallowCopy,
8 +} from 'shared-runtime';
9 +/**
10 + * Function expression version of `aliased-nested-scope-truncated-dep`.
11 + *
12 + * In this fixture, the output would be invalid if propagateScopeDeps did not
13 + * avoid adding MemberExpression dependencies which would other evaluate during
14 + * the mutable ranges of their base objects.
15 + * This is different from `aliased-nested-scope-truncated-dep` which *does*
16 + * produce correct output regardless of MemberExpression dependency truncation.
17 + *
18 + * Note while other expressions evaluate inline, function expressions *always*
19 + * represent deferred evaluation. This means that
20 + * (1) it's always safe to reorder function expression creation until its
21 + * earliest potential invocation
22 + * (2) it's invalid to eagerly evaluate function expression dependencies during
23 + * their respective mutable ranges.
24 + */
25 +
26 +function Component({prop}) {
27 + let obj = shallowCopy(prop);
28 +
29 + const aliasedObj = identity(obj);
30 +
31 + // When `obj` is mutable (either directly or through aliases), taking a
32 + // dependency on `obj.id` is invalid as it may change before getId() is invoked
33 + const getId = () => obj.id;
34 +
35 + mutate(aliasedObj);
36 + setPropertyByKey(aliasedObj, 'id', prop.id + 1);
37 +
38 + // Calling getId() should return prop.id + 1, not the prev
39 + return <Stringify getId={getId} shouldInvokeFns={true} />;
40 +}
41 +
42 +export const FIXTURE_ENTRYPOINT = {
43 + fn: Component,
44 + params: [{prop: {id: 1}}],
45 + sequentialRenders: [{prop: {id: 1}}, {prop: {id: 1}}, {prop: {id: 2}}],
46 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-truncated-dep.expect.md new
+221
@@ -0,0 +1,221 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {
6 + Stringify,
7 + mutate,
8 + identity,
9 + shallowCopy,
10 + setPropertyByKey,
11 +} from 'shared-runtime';
12 +
13 +/**
14 + * This fixture is similar to `bug-aliased-capture-aliased-mutate` and
15 + * `nonmutating-capture-in-unsplittable-memo-block`, but with a focus on
16 + * dependency extraction.
17 + *
18 + * NOTE: this fixture is currently valid, but will break with optimizations:
19 + * - Scope and mutable-range based reordering may move the array creation
20 + * *after* the `mutate(aliasedObj)` call. This is invalid if mutate
21 + * reassigns inner properties.
22 + * - RecycleInto or other deeper-equality optimizations may produce invalid
23 + * output -- it may compare the array's contents / dependencies too early.
24 + * - Runtime validation for immutable values will break if `mutate` does
25 + * interior mutation of the value captured into the array.
26 + *
27 + * Before scope block creation, HIR looks like this:
28 + * //
29 + * // $1 is unscoped as obj's mutable range will be
30 + * // extended in a later pass
31 + * //
32 + * $1 = LoadLocal obj@0[0:12]
33 + * $2 = PropertyLoad $1.id
34 + * //
35 + * // $3 gets assigned a scope as Array is an allocating
36 + * // instruction, but this does *not* get extended or
37 + * // merged into the later mutation site.
38 + * // (explained in `bug-aliased-capture-aliased-mutate`)
39 + * //
40 + * $3@1 = Array[$2]
41 + * ...
42 + * $10@0 = LoadLocal shallowCopy@0[0, 12]
43 + * $11 = LoadGlobal mutate
44 + * $12 = $11($10@0[0, 12])
45 + *
46 + * When filling in scope dependencies, we find that it's incorrect to depend on
47 + * PropertyLoads from obj as it hasn't completed its mutable range. Following
48 + * the immutable / mutable-new typing system, we check the identity of obj to
49 + * detect whether it was newly created (and thus mutable) in this render pass.
50 + *
51 + * HIR with scopes looks like this.
52 + * bb0:
53 + * $1 = LoadLocal obj@0[0:12]
54 + * $2 = PropertyLoad $1.id
55 + * scopeTerminal deps=[obj@0] block=bb1 fallt=bb2
56 + * bb1:
57 + * $3@1 = Array[$2]
58 + * goto bb2
59 + * bb2:
60 + * ...
61 + *
62 + * This is surprising as deps now is entirely decoupled from temporaries used
63 + * by the block itself. scope @1's instructions now reference a value (1)
64 + * produced outside its scope range and (2) not represented in its dependencies
65 + *
66 + * The right thing to do is to ensure that all Loads from a value get assigned
67 + * the value's reactive scope. This also requires track mutating and aliasing
68 + * separately from scope range. In this example, that would correctly merge
69 + * the scopes of $3 with obj.
70 + * Runtime validation and optimizations such as ReactiveGraph-based reordering
71 + * require this as well.
72 + *
73 + * A tempting fix is to instead extend $3's ReactiveScope range up to include
74 + * $2 (the PropertyLoad). This fixes dependency deduping but not reordering
75 + * and mutability.
76 + */
77 +function Component({prop}) {
78 + let obj = shallowCopy(prop);
79 + const aliasedObj = identity(obj);
80 +
81 + // [obj.id] currently is assigned its own reactive scope
82 + const id = [obj.id];
83 +
84 + // Writing to the alias may reassign to previously captured references.
85 + // The compiler currently produces valid output, but this breaks with
86 + // reordering, recycleInto, and other potential optimizations.
87 + mutate(aliasedObj);
88 + setPropertyByKey(aliasedObj, 'id', prop.id + 1);
89 +
90 + return <Stringify id={id} />;
91 +}
92 +
93 +export const FIXTURE_ENTRYPOINT = {
94 + fn: Component,
95 + params: [{prop: {id: 1}}],
96 + sequentialRenders: [{prop: {id: 1}}, {prop: {id: 1}}, {prop: {id: 2}}],
97 +};
98 +
99 +```
100 +
101 +## Code
102 +
103 +```javascript
104 +import { c as _c } from "react/compiler-runtime";
105 +import {
106 + Stringify,
107 + mutate,
108 + identity,
109 + shallowCopy,
110 + setPropertyByKey,
111 +} from "shared-runtime";
112 +
113 +/**
114 + * This fixture is similar to `bug-aliased-capture-aliased-mutate` and
115 + * `nonmutating-capture-in-unsplittable-memo-block`, but with a focus on
116 + * dependency extraction.
117 + *
118 + * NOTE: this fixture is currently valid, but will break with optimizations:
119 + * - Scope and mutable-range based reordering may move the array creation
120 + * *after* the `mutate(aliasedObj)` call. This is invalid if mutate
121 + * reassigns inner properties.
122 + * - RecycleInto or other deeper-equality optimizations may produce invalid
123 + * output -- it may compare the array's contents / dependencies too early.
124 + * - Runtime validation for immutable values will break if `mutate` does
125 + * interior mutation of the value captured into the array.
126 + *
127 + * Before scope block creation, HIR looks like this:
128 + * //
129 + * // $1 is unscoped as obj's mutable range will be
130 + * // extended in a later pass
131 + * //
132 + * $1 = LoadLocal obj@0[0:12]
133 + * $2 = PropertyLoad $1.id
134 + * //
135 + * // $3 gets assigned a scope as Array is an allocating
136 + * // instruction, but this does *not* get extended or
137 + * // merged into the later mutation site.
138 + * // (explained in `bug-aliased-capture-aliased-mutate`)
139 + * //
140 + * $3@1 = Array[$2]
141 + * ...
142 + * $10@0 = LoadLocal shallowCopy@0[0, 12]
143 + * $11 = LoadGlobal mutate
144 + * $12 = $11($10@0[0, 12])
145 + *
146 + * When filling in scope dependencies, we find that it's incorrect to depend on
147 + * PropertyLoads from obj as it hasn't completed its mutable range. Following
148 + * the immutable / mutable-new typing system, we check the identity of obj to
149 + * detect whether it was newly created (and thus mutable) in this render pass.
150 + *
151 + * HIR with scopes looks like this.
152 + * bb0:
153 + * $1 = LoadLocal obj@0[0:12]
154 + * $2 = PropertyLoad $1.id
155 + * scopeTerminal deps=[obj@0] block=bb1 fallt=bb2
156 + * bb1:
157 + * $3@1 = Array[$2]
158 + * goto bb2
159 + * bb2:
160 + * ...
161 + *
162 + * This is surprising as deps now is entirely decoupled from temporaries used
163 + * by the block itself. scope @1's instructions now reference a value (1)
164 + * produced outside its scope range and (2) not represented in its dependencies
165 + *
166 + * The right thing to do is to ensure that all Loads from a value get assigned
167 + * the value's reactive scope. This also requires track mutating and aliasing
168 + * separately from scope range. In this example, that would correctly merge
169 + * the scopes of $3 with obj.
170 + * Runtime validation and optimizations such as ReactiveGraph-based reordering
171 + * require this as well.
172 + *
173 + * A tempting fix is to instead extend $3's ReactiveScope range up to include
174 + * $2 (the PropertyLoad). This fixes dependency deduping but not reordering
175 + * and mutability.
176 + */
177 +function Component(t0) {
178 + const $ = _c(4);
179 + const { prop } = t0;
180 + let t1;
181 + if ($[0] !== prop) {
182 + const obj = shallowCopy(prop);
183 + const aliasedObj = identity(obj);
184 + let t2;
185 + if ($[2] !== obj) {
186 + t2 = [obj.id];
187 + $[2] = obj;
188 + $[3] = t2;
189 + } else {
190 + t2 = $[3];
191 + }
192 + const id = t2;
193 +
194 + mutate(aliasedObj);
195 + setPropertyByKey(aliasedObj, "id", prop.id + 1);
196 +
197 + t1 = <Stringify id={id} />;
198 + $[0] = prop;
199 + $[1] = t1;
200 + } else {
201 + t1 = $[1];
202 + }
203 + return t1;
204 +}
205 +
206 +export const FIXTURE_ENTRYPOINT = {
207 + fn: Component,
208 + params: [{ prop: { id: 1 } }],
209 + sequentialRenders: [
210 + { prop: { id: 1 } },
211 + { prop: { id: 1 } },
212 + { prop: { id: 2 } },
213 + ],
214 +};
215 +
216 +```
217 +
218 +### Eval output
219 +(kind: ok) <div>{"id":[1]}</div>
220 +<div>{"id":[1]}</div>
221 +<div>{"id":[2]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-truncated-dep.tsx new
+93
@@ -0,0 +1,93 @@
1 +import {
2 + Stringify,
3 + mutate,
4 + identity,
5 + shallowCopy,
6 + setPropertyByKey,
7 +} from 'shared-runtime';
8 +
9 +/**
10 + * This fixture is similar to `bug-aliased-capture-aliased-mutate` and
11 + * `nonmutating-capture-in-unsplittable-memo-block`, but with a focus on
12 + * dependency extraction.
13 + *
14 + * NOTE: this fixture is currently valid, but will break with optimizations:
15 + * - Scope and mutable-range based reordering may move the array creation
16 + * *after* the `mutate(aliasedObj)` call. This is invalid if mutate
17 + * reassigns inner properties.
18 + * - RecycleInto or other deeper-equality optimizations may produce invalid
19 + * output -- it may compare the array's contents / dependencies too early.
20 + * - Runtime validation for immutable values will break if `mutate` does
21 + * interior mutation of the value captured into the array.
22 + *
23 + * Before scope block creation, HIR looks like this:
24 + * //
25 + * // $1 is unscoped as obj's mutable range will be
26 + * // extended in a later pass
27 + * //
28 + * $1 = LoadLocal obj@0[0:12]
29 + * $2 = PropertyLoad $1.id
30 + * //
31 + * // $3 gets assigned a scope as Array is an allocating
32 + * // instruction, but this does *not* get extended or
33 + * // merged into the later mutation site.
34 + * // (explained in `bug-aliased-capture-aliased-mutate`)
35 + * //
36 + * $3@1 = Array[$2]
37 + * ...
38 + * $10@0 = LoadLocal shallowCopy@0[0, 12]
39 + * $11 = LoadGlobal mutate
40 + * $12 = $11($10@0[0, 12])
41 + *
42 + * When filling in scope dependencies, we find that it's incorrect to depend on
43 + * PropertyLoads from obj as it hasn't completed its mutable range. Following
44 + * the immutable / mutable-new typing system, we check the identity of obj to
45 + * detect whether it was newly created (and thus mutable) in this render pass.
46 + *
47 + * HIR with scopes looks like this.
48 + * bb0:
49 + * $1 = LoadLocal obj@0[0:12]
50 + * $2 = PropertyLoad $1.id
51 + * scopeTerminal deps=[obj@0] block=bb1 fallt=bb2
52 + * bb1:
53 + * $3@1 = Array[$2]
54 + * goto bb2
55 + * bb2:
56 + * ...
57 + *
58 + * This is surprising as deps now is entirely decoupled from temporaries used
59 + * by the block itself. scope @1's instructions now reference a value (1)
60 + * produced outside its scope range and (2) not represented in its dependencies
61 + *
62 + * The right thing to do is to ensure that all Loads from a value get assigned
63 + * the value's reactive scope. This also requires track mutating and aliasing
64 + * separately from scope range. In this example, that would correctly merge
65 + * the scopes of $3 with obj.
66 + * Runtime validation and optimizations such as ReactiveGraph-based reordering
67 + * require this as well.
68 + *
69 + * A tempting fix is to instead extend $3's ReactiveScope range up to include
70 + * $2 (the PropertyLoad). This fixes dependency deduping but not reordering
71 + * and mutability.
72 + */
73 +function Component({prop}) {
74 + let obj = shallowCopy(prop);
75 + const aliasedObj = identity(obj);
76 +
77 + // [obj.id] currently is assigned its own reactive scope
78 + const id = [obj.id];
79 +
80 + // Writing to the alias may reassign to previously captured references.
81 + // The compiler currently produces valid output, but this breaks with
82 + // reordering, recycleInto, and other potential optimizations.
83 + mutate(aliasedObj);
84 + setPropertyByKey(aliasedObj, 'id', prop.id + 1);
85 +
86 + return <Stringify id={id} />;
87 +}
88 +
89 +export const FIXTURE_ENTRYPOINT = {
90 + fn: Component,
91 + params: [{prop: {id: 1}}],
92 + sequentialRenders: [{prop: {id: 1}}, {prop: {id: 1}}, {prop: {id: 2}}],
93 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md
+18 -22
@@ -44,48 +44,44 @@ import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRen
44 import { useEffect, useRef, useState } from "react";
45
46 function Component() {
47 - const $ = _c(6);
47 + const $ = _c(5);
48 const ref = useRef(null);
49 const [state, setState] = useState(false);
50 let t0;
51 - let t1;
51 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
53 - t0 = () => {};
54 -
55 - t1 = [];
52 + t0 = [];
53 $[0] = t0;
57 - $[1] = t1;
54 } else {
55 t0 = $[0];
60 - t1 = $[1];
56 }
62 - useEffect(t0, t1);
57 + useEffect(_temp, t0);
58 + let t1;
59 let t2;
64 - let t3;
65 - if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
66 - t2 = () => {
60 + if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
61 + t1 = () => {
62 setState(true);
63 };
69 - t3 = [];
64 + t2 = [];
65 + $[1] = t1;
66 $[2] = t2;
71 - $[3] = t3;
67 } else {
68 + t1 = $[1];
69 t2 = $[2];
74 - t3 = $[3];
70 }
76 - useEffect(t2, t3);
71 + useEffect(t1, t2);
72
78 - const t4 = String(state);
79 - let t5;
80 - if ($[4] !== t4) {
81 - t5 = <Child key={t4} ref={ref} />;
73 + const t3 = String(state);
74 + let t4;
75 + if ($[3] !== t3) {
76 + t4 = <Child key={t3} ref={ref} />;
77 + $[3] = t3;
78 $[4] = t4;
83 - $[5] = t5;
79 } else {
85 - t5 = $[5];
80 + t4 = $[4];
81 }
87 - return t5;
82 + return t4;
83 }
84 +function _temp() {}
85
86 function Child(t0) {
87 const { ref } = t0;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-mutate-2.expect.md
-1
@@ -27,7 +27,6 @@ export const FIXTURE_ENTRYPOINT = {
27 import { c as _c } from "react/compiler-runtime";
28 function component(a, b) {
29 const $ = _c(2);
30 - const y = { b };
30 let z;
31 if ($[0] !== a) {
32 z = { a };
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-no-mutate.expect.md
+10 -2
@@ -31,12 +31,20 @@ export const FIXTURE_ENTRYPOINT = {
31 ```javascript
32 import { c as _c } from "react/compiler-runtime";
33 function Component(t0) {
34 - const $ = _c(3);
34 + const $ = _c(5);
35 const { a, b } = t0;
36 let z;
37 if ($[0] !== a || $[1] !== b) {
38 z = { a };
39 - const y = { b };
39 + let t1;
40 + if ($[3] !== b) {
41 + t1 = { b };
42 + $[3] = b;
43 + $[4] = t1;
44 + } else {
45 + t1 = $[4];
46 + }
47 + const y = t1;
48 const x = function () {
49 z.a = 2;
50 return Math.max(y.b, 0);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-simple-alias-iife.expect.md
-1
@@ -34,7 +34,6 @@ function component(a) {
34 const x = { a };
35 y = {};
36
37 - y;
37 y = x;
38
39 mutate(y);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-2-iife.expect.md
-1
@@ -31,7 +31,6 @@ function bar(a) {
31 const x = [a];
32 y = {};
33
34 - y;
34 y = x[0][1];
35 $[0] = a;
36 $[1] = y;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-3-iife.expect.md
-2
@@ -37,8 +37,6 @@ function bar(a, b) {
37 let t;
38 t = {};
39
40 - y;
41 - t;
40 y = x[0][1];
41 t = x[1][0];
42 $[0] = a;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-4-iife.expect.md
-1
@@ -31,7 +31,6 @@ function bar(a) {
31 const x = [a];
32 y = {};
33
34 - y;
34 y = x[0].a[1];
35 $[0] = a;
36 $[1] = y;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-iife.expect.md
-1
@@ -30,7 +30,6 @@ function bar(a) {
30 const x = [a];
31 y = {};
32
33 - y;
33 y = x[0];
34 $[0] = a;
35 $[1] = y;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-reference-changes-type.expect.md
-1
@@ -25,7 +25,6 @@ function component(a) {
25 const x = { a };
26 y = 1;
27
28 - y;
28 y = x;
29
30 mutate(y);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-reassign.expect.md
+1 -2
@@ -38,9 +38,8 @@ function useTest() {
38
39 const t1 = (w = 42);
40 const t2 = w;
41 -
42 - w;
41 let t3;
42 +
43 w = 999;
44 t3 = 2;
45 t0 = makeArray(t1, t2, t3);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/const-propagation-into-function-expression-global.expect.md
+5 -2
@@ -19,10 +19,10 @@ function foo() {
19 import { c as _c } from "react/compiler-runtime";
20 function foo() {
21 const $ = _c(1);
22 +
23 + const getJSX = _temp;
24 let t0;
25 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
24 - const getJSX = () => <Child x={GLOBAL_IS_X} />;
25 -
26 t0 = getJSX();
27 $[0] = t0;
28 } else {
@@ -31,6 +31,9 @@ function foo() {
31 const result = t0;
32 return result;
33 }
34 +function _temp() {
35 + return <Child x={GLOBAL_IS_X} />;
36 +}
37
38 ```
39
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/const-propagation-into-function-expression-primitive.expect.md
+4 -3
@@ -23,13 +23,14 @@ export const FIXTURE_ENTRYPOINT = {
23
24 ```javascript
25 function foo() {
26 - const f = () => {
27 - console.log(42);
28 - };
26 + const f = _temp;
27
28 f();
29 return 42;
30 }
31 +function _temp() {
32 + console.log(42);
33 +}
34
35 export const FIXTURE_ENTRYPOINT = {
36 fn: foo,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-into-function-expressions.expect.md
+5 -4
@@ -18,12 +18,10 @@ function Component(props) {
18 import { c as _c } from "react/compiler-runtime";
19 function Component(props) {
20 const $ = _c(1);
21 +
22 + const onEvent = _temp;
23 let t0;
24 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
23 - const onEvent = () => {
24 - console.log(42);
25 - };
26 -
25 t0 = <Foo onEvent={onEvent} />;
26 $[0] = t0;
27 } else {
@@ -31,6 +29,9 @@ function Component(props) {
29 }
30 return t0;
31 }
32 +function _temp() {
33 + console.log(42);
34 +}
35
36 ```
37
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-as-jsx-element-tag.expect.md
+1 -2
@@ -34,9 +34,8 @@ function Component(props) {
34 let Component;
35 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
36 Component = Stringify;
37 -
38 - Component;
37 let t0;
38 +
39 t0 = Component;
40 Component = t0;
41 $[0] = Component;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md
+7 -3
@@ -24,13 +24,17 @@ export const FIXTURE_ENTRYPOINT = {
24 ## Error
25
26 ```
27 + 4 | }
28 5 | return baz(); // OK: FuncDecls are HoistableDeclarations that have both declaration and value hoisting
28 - 6 | function baz() {
29 +> 6 | function baz() {
30 + | ^^^^^^^^^^^^^^^^
31 > 7 | return bar();
30 - | ^^^ Todo: Support functions with unreachable code that may contain hoisted declarations (7:7)
31 - 8 | }
32 + | ^^^^^^^^^^^^^^^^^
33 +> 8 | }
34 + | ^^^^ Todo: Support functions with unreachable code that may contain hoisted declarations (6:8)
35 9 | }
36 10 |
37 + 11 | export const FIXTURE_ENTRYPOINT = {
38 ```
39
40
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.expect.md new
+41
@@ -0,0 +1,41 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableTransitivelyFreezeFunctionExpressions
6 +import {setPropertyByKey, Stringify, useIdentity} from 'shared-runtime';
7 +
8 +function Foo({count}) {
9 + const x = {value: 0};
10 + /**
11 + * After this custom hook call, it's no longer valid to mutate x.
12 + */
13 + const cb = useIdentity(() => {
14 + setPropertyByKey(x, 'value', count);
15 + });
16 +
17 + x.value += count;
18 + return <Stringify x={x} cb={cb} />;
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: Foo,
23 + params: [{count: 1}],
24 +};
25 +
26 +```
27 +
28 +
29 +## Error
30 +
31 +```
32 + 11 | });
33 + 12 |
34 +> 13 | x.value += count;
35 + | ^ InvalidReact: This mutates a variable that React considers immutable (13:13)
36 + 14 | return <Stringify x={x} cb={cb} />;
37 + 15 | }
38 + 16 |
39 +```
40 +
41 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.tsx new
+20
@@ -0,0 +1,20 @@
1 +// @enableTransitivelyFreezeFunctionExpressions
2 +import {setPropertyByKey, Stringify, useIdentity} from 'shared-runtime';
3 +
4 +function Foo({count}) {
5 + const x = {value: 0};
6 + /**
7 + * After this custom hook call, it's no longer valid to mutate x.
8 + */
9 + const cb = useIdentity(() => {
10 + setPropertyByKey(x, 'value', count);
11 + });
12 +
13 + x.value += count;
14 + return <Stringify x={x} cb={cb} />;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Foo,
19 + params: [{count: 1}],
20 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.expect.md new
+41
@@ -0,0 +1,41 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableTransitivelyFreezeFunctionExpressions
6 +import {mutate, Stringify, useIdentity} from 'shared-runtime';
7 +
8 +function Foo({count}) {
9 + const x = {value: 0};
10 + /**
11 + * After this custom hook call, it's no longer valid to mutate x.
12 + */
13 + const cb = useIdentity(() => {
14 + x.value++;
15 + });
16 +
17 + x.value += count;
18 + return <Stringify x={x} cb={cb} />;
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: Foo,
23 + params: [{count: 1}],
24 +};
25 +
26 +```
27 +
28 +
29 +## Error
30 +
31 +```
32 + 11 | });
33 + 12 |
34 +> 13 | x.value += count;
35 + | ^ InvalidReact: This mutates a variable that React considers immutable (13:13)
36 + 14 | return <Stringify x={x} cb={cb} />;
37 + 15 | }
38 + 16 |
39 +```
40 +
41 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.jsx new
+20
@@ -0,0 +1,20 @@
1 +// @enableTransitivelyFreezeFunctionExpressions
2 +import {mutate, Stringify, useIdentity} from 'shared-runtime';
3 +
4 +function Foo({count}) {
5 + const x = {value: 0};
6 + /**
7 + * After this custom hook call, it's no longer valid to mutate x.
8 + */
9 + const cb = useIdentity(() => {
10 + x.value++;
11 + });
12 +
13 + x.value += count;
14 + return <Stringify x={x} cb={cb} />;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Foo,
19 + params: [{count: 1}],
20 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md
+1 -1
@@ -22,7 +22,7 @@ function Component(props) {
22 7 | return hasErrors;
23 8 | }
24 > 9 | return hasErrors();
25 - | ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$16 (9:9)
25 + | ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$14 (9:9)
26 10 | }
27 11 |
28 ```
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate-captured-indirectly-jsx.expect.md new
+81
@@ -0,0 +1,81 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +function useFoo() {
7 + const onClick = response => {
8 + setState(DISABLED_FORM);
9 + };
10 +
11 + const [state, setState] = useState();
12 + const handleLogout = useCallback(() => {
13 + setState(DISABLED_FORM);
14 + }, [setState]);
15 + const getComponent = () => {
16 + return <ColumnItem onPress={() => handleLogout()} />;
17 + };
18 +
19 + // this `getComponent` call should not be inferred as mutating setState
20 + return [getComponent(), onClick]; // pass onClick to avoid dce
21 +}
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
29 +function useFoo() {
30 + const $ = _c(9);
31 + const onClick = (response) => {
32 + setState(DISABLED_FORM);
33 + };
34 +
35 + const [, t0] = useState();
36 + const setState = t0;
37 + let t1;
38 + if ($[0] !== setState) {
39 + t1 = () => {
40 + setState(DISABLED_FORM);
41 + };
42 + $[0] = setState;
43 + $[1] = t1;
44 + } else {
45 + t1 = $[1];
46 + }
47 + setState;
48 + const handleLogout = t1;
49 + let t2;
50 + if ($[2] !== handleLogout) {
51 + t2 = () => <ColumnItem onPress={() => handleLogout()} />;
52 + $[2] = handleLogout;
53 + $[3] = t2;
54 + } else {
55 + t2 = $[3];
56 + }
57 + const getComponent = t2;
58 + let t3;
59 + if ($[4] !== getComponent) {
60 + t3 = getComponent();
61 + $[4] = getComponent;
62 + $[5] = t3;
63 + } else {
64 + t3 = $[5];
65 + }
66 + let t4;
67 + if ($[6] !== onClick || $[7] !== t3) {
68 + t4 = [t3, onClick];
69 + $[6] = onClick;
70 + $[7] = t3;
71 + $[8] = t4;
72 + } else {
73 + t4 = $[8];
74 + }
75 + return t4;
76 +}
77 +
78 +```
79 +
80 +### Eval output
81 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate-captured-indirectly-jsx.js new
+17
@@ -0,0 +1,17 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +function useFoo() {
3 + const onClick = response => {
4 + setState(DISABLED_FORM);
5 + };
6 +
7 + const [state, setState] = useState();
8 + const handleLogout = useCallback(() => {
9 + setState(DISABLED_FORM);
10 + }, [setState]);
11 + const getComponent = () => {
12 + return <ColumnItem onPress={() => handleLogout()} />;
13 + };
14 +
15 + // this `getComponent` call should not be inferred as mutating setState
16 + return [getComponent(), onClick]; // pass onClick to avoid dce
17 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate.expect.md new
+77
@@ -0,0 +1,77 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useEffect, useState} from 'react';
6 +import {Stringify} from 'shared-runtime';
7 +
8 +function Foo() {
9 + /**
10 + * Previously, this lowered to
11 + * $1 = LoadContext capture setState
12 + * $2 = FunctionExpression deps=$1 context=setState
13 + * [[ at this point, we freeze the `LoadContext setState` instruction, but it will never be referenced again ]]
14 + *
15 + * Now, this function expression directly references `setState`, which freezes
16 + * the source `DeclareContext HoistedConst setState`. Freezing source identifiers
17 + * (instead of the one level removed `LoadContext`) is more semantically correct
18 + * for everything *other* than hoisted context declarations.
19 + *
20 + * $2 = Function context=setState
21 + */
22 + useEffect(() => setState(2), []);
23 +
24 + const [state, setState] = useState(0);
25 + return <Stringify state={state} />;
26 +}
27 +
28 +export const FIXTURE_ENTRYPOINT = {
29 + fn: Foo,
30 + params: [{}],
31 + sequentialRenders: [{}, {}],
32 +};
33 +
34 +```
35 +
36 +## Code
37 +
38 +```javascript
39 +import { c as _c } from "react/compiler-runtime";
40 +import { useEffect, useState } from "react";
41 +import { Stringify } from "shared-runtime";
42 +
43 +function Foo() {
44 + const $ = _c(3);
45 + let t0;
46 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
47 + t0 = [];
48 + $[0] = t0;
49 + } else {
50 + t0 = $[0];
51 + }
52 + useEffect(() => setState(2), t0);
53 +
54 + const [state, t1] = useState(0);
55 + const setState = t1;
56 + let t2;
57 + if ($[1] !== state) {
58 + t2 = <Stringify state={state} />;
59 + $[1] = state;
60 + $[2] = t2;
61 + } else {
62 + t2 = $[2];
63 + }
64 + return t2;
65 +}
66 +
67 +export const FIXTURE_ENTRYPOINT = {
68 + fn: Foo,
69 + params: [{}],
70 + sequentialRenders: [{}, {}],
71 +};
72 +
73 +```
74 +
75 +### Eval output
76 +(kind: ok) <div>{"state":2}</div>
77 +<div>{"state":2}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate.js new
+28
@@ -0,0 +1,28 @@
1 +import {useEffect, useState} from 'react';
2 +import {Stringify} from 'shared-runtime';
3 +
4 +function Foo() {
5 + /**
6 + * Previously, this lowered to
7 + * $1 = LoadContext capture setState
8 + * $2 = FunctionExpression deps=$1 context=setState
9 + * [[ at this point, we freeze the `LoadContext setState` instruction, but it will never be referenced again ]]
10 + *
11 + * Now, this function expression directly references `setState`, which freezes
12 + * the source `DeclareContext HoistedConst setState`. Freezing source identifiers
13 + * (instead of the one level removed `LoadContext`) is more semantically correct
14 + * for everything *other* than hoisted context declarations.
15 + *
16 + * $2 = Function context=setState
17 + */
18 + useEffect(() => setState(2), []);
19 +
20 + const [state, setState] = useState(0);
21 + return <Stringify state={state} />;
22 +}
23 +
24 +export const FIXTURE_ENTRYPOINT = {
25 + fn: Foo,
26 + params: [{}],
27 + sequentialRenders: [{}, {}],
28 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-call-freezes-captured-memberexpr.expect.md new
+87
@@ -0,0 +1,87 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useIdentity, Stringify, identity} from 'shared-runtime';
6 +
7 +function Foo({val1}) {
8 + // `x={inner: val1}` should be able to be memoized
9 + const x = {inner: val1};
10 +
11 + // Any references to `x` after this hook call should be read-only
12 + const cb = useIdentity(() => x.inner);
13 +
14 + // With enableTransitivelyFreezeFunctionExpressions, it's invalid
15 + // to write to `x` after it's been frozen.
16 + // TODO: runtime validation for DX
17 + const copy = identity(x);
18 + return <Stringify copy={copy} cb={cb} shouldInvokeFns={true} />;
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: Foo,
23 + params: [{val1: 1}],
24 + sequentialRenders: [{val1: 1}, {val1: 1}],
25 +};
26 +
27 +```
28 +
29 +## Code
30 +
31 +```javascript
32 +import { c as _c } from "react/compiler-runtime";
33 +import { useIdentity, Stringify, identity } from "shared-runtime";
34 +
35 +function Foo(t0) {
36 + const $ = _c(9);
37 + const { val1 } = t0;
38 + let t1;
39 + if ($[0] !== val1) {
40 + t1 = { inner: val1 };
41 + $[0] = val1;
42 + $[1] = t1;
43 + } else {
44 + t1 = $[1];
45 + }
46 + const x = t1;
47 + let t2;
48 + if ($[2] !== x.inner) {
49 + t2 = () => x.inner;
50 + $[2] = x.inner;
51 + $[3] = t2;
52 + } else {
53 + t2 = $[3];
54 + }
55 + const cb = useIdentity(t2);
56 + let t3;
57 + if ($[4] !== x) {
58 + t3 = identity(x);
59 + $[4] = x;
60 + $[5] = t3;
61 + } else {
62 + t3 = $[5];
63 + }
64 + const copy = t3;
65 + let t4;
66 + if ($[6] !== cb || $[7] !== copy) {
67 + t4 = <Stringify copy={copy} cb={cb} shouldInvokeFns={true} />;
68 + $[6] = cb;
69 + $[7] = copy;
70 + $[8] = t4;
71 + } else {
72 + t4 = $[8];
73 + }
74 + return t4;
75 +}
76 +
77 +export const FIXTURE_ENTRYPOINT = {
78 + fn: Foo,
79 + params: [{ val1: 1 }],
80 + sequentialRenders: [{ val1: 1 }, { val1: 1 }],
81 +};
82 +
83 +```
84 +
85 +### Eval output
86 +(kind: ok) <div>{"copy":{"inner":1},"cb":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
87 +<div>{"copy":{"inner":1},"cb":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-call-freezes-captured-memberexpr.tsx new
+21
@@ -0,0 +1,21 @@
1 +import {useIdentity, Stringify, identity} from 'shared-runtime';
2 +
3 +function Foo({val1}) {
4 + // `x={inner: val1}` should be able to be memoized
5 + const x = {inner: val1};
6 +
7 + // Any references to `x` after this hook call should be read-only
8 + const cb = useIdentity(() => x.inner);
9 +
10 + // With enableTransitivelyFreezeFunctionExpressions, it's invalid
11 + // to write to `x` after it's been frozen.
12 + // TODO: runtime validation for DX
13 + const copy = identity(x);
14 + return <Stringify copy={copy} cb={cb} shouldInvokeFns={true} />;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Foo,
19 + params: [{val1: 1}],
20 + sequentialRenders: [{val1: 1}, {val1: 1}],
21 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-tag-in-lambda.expect.md
+5 -2
@@ -25,10 +25,10 @@ import { c as _c } from "react/compiler-runtime";
25 import { Stringify } from "shared-runtime";
26 function useFoo() {
27 const $ = _c(1);
28 +
29 + const callback = _temp;
30 let t0;
31 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
30 - const callback = () => <Stringify value={4} />;
31 -
32 t0 = callback();
33 $[0] = t0;
34 } else {
@@ -36,6 +36,9 @@ function useFoo() {
36 }
37 return t0;
38 }
39 +function _temp() {
40 + return <Stringify value={4} />;
41 +}
42
43 export const FIXTURE_ENTRYPOINT = {
44 fn: useFoo,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-memberexpr-tag-in-lambda.expect.md
+5 -2
@@ -25,10 +25,10 @@ import { c as _c } from "react/compiler-runtime";
25 import * as SharedRuntime from "shared-runtime";
26 function useFoo() {
27 const $ = _c(1);
28 +
29 + const callback = _temp;
30 let t0;
31 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
30 - const callback = () => <SharedRuntime.Text value={4} />;
31 -
32 t0 = callback();
33 $[0] = t0;
34 } else {
@@ -36,6 +36,9 @@ function useFoo() {
36 }
37 return t0;
38 }
39 +function _temp() {
40 + return <SharedRuntime.Text value={4} />;
41 +}
42
43 export const FIXTURE_ENTRYPOINT = {
44 fn: useFoo,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/lambda-mutated-non-reactive-to-reactive.expect.md
-1
@@ -26,7 +26,6 @@ function f(a) {
26 const $ = _c(4);
27 let x;
28 if ($[0] !== a) {
29 - x;
29 x = { a };
30 $[0] = a;
31 $[1] = x;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/lambda-mutated-ref-non-reactive.expect.md
-1
@@ -27,7 +27,6 @@ function f(a) {
27 const $ = _c(2);
28 let x;
29 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
30 - x;
30 x = {};
31 $[0] = x;
32 } else {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-function-shadowed-identifiers.expect.md
+4 -1
@@ -31,7 +31,7 @@ function Component(props) {
31 let t0;
32 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
33 t0 = (e) => {
34 - setX((currentX) => currentX + null);
34 + setX(_temp);
35 };
36 $[0] = t0;
37 } else {
@@ -48,6 +48,9 @@ function Component(props) {
48 }
49 return t1;
50 }
51 +function _temp(currentX) {
52 + return currentX + null;
53 +}
54
55 export const FIXTURE_ENTRYPOINT = {
56 fn: Component,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-assignment.expect.md
-1
@@ -35,7 +35,6 @@ function useFoo(arr1, arr2) {
35 if ($[0] !== arr1 || $[1] !== arr2) {
36 const x = [arr1];
37
38 - y;
38 (y = x.concat(arr2)), y;
39 $[0] = arr1;
40 $[1] = arr2;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rewrite-phis-in-lambda-capture-context.expect.md
+12 -17
@@ -22,26 +22,21 @@ function Component() {
22 ## Code
23
24 ```javascript
25 -import { c as _c } from "react/compiler-runtime";
25 function Component() {
27 - const $ = _c(1);
28 - let t0;
29 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
30 - t0 = () => {
31 - while (bar()) {
32 - if (baz) {
33 - bar();
34 - }
35 - }
36 - return () => 4;
37 - };
38 - $[0] = t0;
39 - } else {
40 - t0 = $[0];
41 - }
42 - const get4 = t0;
26 + const get4 = _temp2;
27 return get4;
28 }
29 +function _temp2() {
30 + while (bar()) {
31 + if (baz) {
32 + bar();
33 + }
34 + }
35 + return _temp;
36 +}
37 +function _temp() {
38 + return 4;
39 +}
40
41 ```
42
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-conditional.expect.md
-1
@@ -85,7 +85,6 @@ function Inner(props) {
85 input = use(FooContext);
86 }
87
88 - input;
88 input;
89 let t0;
90 let t1;