Skip to content

Commit 04a7a61

Browse files
authored
[compiler] Allow assigning ref-accessing functions to objects if not mutated (facebook#34026)
Allows assigning a ref-accessing function to an object so long as that object is not subsequently transitively mutated. We should likely rewrite the ref validation to use the new mutation/aliasing effects, which would provide a more consistent behavior across instruction types and require fewer special cases like this. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34026). * facebook#34027 * __->__ facebook#34026
1 parent c2326b1 commit 04a7a61

6 files changed

+196
-19
lines changed

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts

Lines changed: 79 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,18 @@ type RefAccessRefType =
8080

8181
type RefFnType = {readRefEffect: boolean; returnType: RefAccessType};
8282

83-
class Env extends Map<IdentifierId, RefAccessType> {
83+
class Env {
8484
#changed = false;
85+
#data: Map<IdentifierId, RefAccessType> = new Map();
86+
#temporaries: Map<IdentifierId, Place> = new Map();
87+
88+
lookup(place: Place): Place {
89+
return this.#temporaries.get(place.identifier.id) ?? place;
90+
}
91+
92+
define(place: Place, value: Place): void {
93+
this.#temporaries.set(place.identifier.id, value);
94+
}
8595

8696
resetChanged(): void {
8797
this.#changed = false;
@@ -91,26 +101,72 @@ class Env extends Map<IdentifierId, RefAccessType> {
91101
return this.#changed;
92102
}
93103

94-
override set(key: IdentifierId, value: RefAccessType): this {
95-
const cur = this.get(key);
104+
get(key: IdentifierId): RefAccessType | undefined {
105+
const operandId = this.#temporaries.get(key)?.identifier.id ?? key;
106+
return this.#data.get(operandId);
107+
}
108+
109+
set(key: IdentifierId, value: RefAccessType): this {
110+
const operandId = this.#temporaries.get(key)?.identifier.id ?? key;
111+
const cur = this.#data.get(operandId);
96112
const widenedValue = joinRefAccessTypes(value, cur ?? {kind: 'None'});
97113
if (
98114
!(cur == null && widenedValue.kind === 'None') &&
99115
(cur == null || !tyEqual(cur, widenedValue))
100116
) {
101117
this.#changed = true;
102118
}
103-
return super.set(key, widenedValue);
119+
this.#data.set(operandId, widenedValue);
120+
return this;
104121
}
105122
}
106123

107124
export function validateNoRefAccessInRender(
108125
fn: HIRFunction,
109126
): Result<void, CompilerError> {
110127
const env = new Env();
128+
collectTemporariesSidemap(fn, env);
111129
return validateNoRefAccessInRenderImpl(fn, env).map(_ => undefined);
112130
}
113131

132+
function collectTemporariesSidemap(fn: HIRFunction, env: Env): void {
133+
for (const block of fn.body.blocks.values()) {
134+
for (const instr of block.instructions) {
135+
const {lvalue, value} = instr;
136+
switch (value.kind) {
137+
case 'LoadLocal': {
138+
const temp = env.lookup(value.place);
139+
if (temp != null) {
140+
env.define(lvalue, temp);
141+
}
142+
break;
143+
}
144+
case 'StoreLocal': {
145+
const temp = env.lookup(value.value);
146+
if (temp != null) {
147+
env.define(lvalue, temp);
148+
env.define(value.lvalue.place, temp);
149+
}
150+
break;
151+
}
152+
case 'PropertyLoad': {
153+
if (
154+
isUseRefType(value.object.identifier) &&
155+
value.property === 'current'
156+
) {
157+
continue;
158+
}
159+
const temp = env.lookup(value.object);
160+
if (temp != null) {
161+
env.define(lvalue, temp);
162+
}
163+
break;
164+
}
165+
}
166+
}
167+
}
168+
}
169+
114170
function refTypeOfType(place: Place): RefAccessType {
115171
if (isRefValueType(place.identifier)) {
116172
return {kind: 'RefValue'};
@@ -524,11 +580,25 @@ function validateNoRefAccessInRenderImpl(
524580
} else {
525581
validateNoRefUpdate(errors, env, instr.value.object, instr.loc);
526582
}
527-
for (const operand of eachInstructionValueOperand(instr.value)) {
528-
if (operand === instr.value.object) {
529-
continue;
583+
if (
584+
instr.value.kind === 'ComputedDelete' ||
585+
instr.value.kind === 'ComputedStore'
586+
) {
587+
validateNoRefValueAccess(errors, env, instr.value.property);
588+
}
589+
if (
590+
instr.value.kind === 'ComputedStore' ||
591+
instr.value.kind === 'PropertyStore'
592+
) {
593+
validateNoDirectRefValueAccess(errors, instr.value.value, env);
594+
const type = env.get(instr.value.value.identifier.id);
595+
if (type != null && type.kind === 'Structure') {
596+
let objectType: RefAccessType = type;
597+
if (target != null) {
598+
objectType = joinRefAccessTypes(objectType, target);
599+
}
600+
env.set(instr.value.object.identifier.id, objectType);
530601
}
531-
validateNoRefValueAccess(errors, env, operand);
532602
}
533603
break;
534604
}
@@ -730,11 +800,7 @@ function validateNoRefUpdate(
730800
loc: SourceLocation,
731801
): void {
732802
const type = destructure(env.get(operand.identifier.id));
733-
if (
734-
type?.kind === 'Ref' ||
735-
type?.kind === 'RefValue' ||
736-
(type?.kind === 'Structure' && type.fn?.readRefEffect)
737-
) {
803+
if (type?.kind === 'Ref' || type?.kind === 'RefValue') {
738804
errors.pushDiagnostic(
739805
CompilerDiagnostic.create({
740806
severity: ErrorSeverity.InvalidReact,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
2+
## Input
3+
4+
```javascript
5+
import {useRef} from 'react';
6+
import {Stringify} from 'shared-runtime';
7+
8+
function Component(props) {
9+
const ref = useRef(props.value);
10+
const object = {};
11+
object.foo = () => ref.current;
12+
return <Stringify object={object} shouldInvokeFns={true} />;
13+
}
14+
15+
export const FIXTURE_ENTRYPOINT = {
16+
fn: Component,
17+
params: [{value: 42}],
18+
};
19+
20+
```
21+
22+
## Code
23+
24+
```javascript
25+
import { c as _c } from "react/compiler-runtime";
26+
import { useRef } from "react";
27+
import { Stringify } from "shared-runtime";
28+
29+
function Component(props) {
30+
const $ = _c(1);
31+
const ref = useRef(props.value);
32+
let t0;
33+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
34+
const object = {};
35+
object.foo = () => ref.current;
36+
t0 = <Stringify object={object} shouldInvokeFns={true} />;
37+
$[0] = t0;
38+
} else {
39+
t0 = $[0];
40+
}
41+
return t0;
42+
}
43+
44+
export const FIXTURE_ENTRYPOINT = {
45+
fn: Component,
46+
params: [{ value: 42 }],
47+
};
48+
49+
```
50+
51+
### Eval output
52+
(kind: ok) <div>{"object":{"foo":{"kind":"Function","result":42}},"shouldInvokeFns":true}</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {useRef} from 'react';
2+
import {Stringify} from 'shared-runtime';
3+
4+
function Component(props) {
5+
const ref = useRef(props.value);
6+
const object = {};
7+
object.foo = () => ref.current;
8+
return <Stringify object={object} shouldInvokeFns={true} />;
9+
}
10+
11+
export const FIXTURE_ENTRYPOINT = {
12+
fn: Component,
13+
params: [{value: 42}],
14+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
## Input
3+
4+
```javascript
5+
import {useRef} from 'react';
6+
7+
function Component() {
8+
const ref = useRef(null);
9+
const object = {};
10+
object.foo = () => ref.current;
11+
const refValue = object.foo();
12+
return <div>{refValue}</div>;
13+
}
14+
15+
```
16+
17+
18+
## Error
19+
20+
```
21+
Found 1 error:
22+
23+
Error: Cannot access refs during render
24+
25+
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
26+
27+
error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:7:19
28+
5 | const object = {};
29+
6 | object.foo = () => ref.current;
30+
> 7 | const refValue = object.foo();
31+
| ^^^^^^^^^^ This function accesses a ref value
32+
8 | return <div>{refValue}</div>;
33+
9 | }
34+
10 |
35+
```
36+
37+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {useRef} from 'react';
2+
3+
function Component() {
4+
const ref = useRef(null);
5+
const object = {};
6+
object.foo = () => ref.current;
7+
const refValue = object.foo();
8+
return <div>{refValue}</div>;
9+
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,13 @@ Error: Cannot access refs during render
4141
4242
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
4343
44-
error.invalid-use-ref-added-to-dep-without-type-info.ts:10:21
45-
8 | // however, this is an instance of accessing a ref during render and is disallowed
46-
9 | // under React's rules, so we reject this input
47-
> 10 | const x = {a, val: val.ref.current};
48-
| ^^^^^^^^^^^^^^^ Cannot access ref value during render
44+
error.invalid-use-ref-added-to-dep-without-type-info.ts:12:28
45+
10 | const x = {a, val: val.ref.current};
4946
11 |
50-
12 | return <VideoList videos={x} />;
47+
> 12 | return <VideoList videos={x} />;
48+
| ^ Cannot access ref value during render
5149
13 | }
50+
14 |
5251
```
5352
5453

0 commit comments

Comments
 (0)