[compiler] Fix bug w functions depending on hoisted primitives (#35284)

Fixes an edge case where a function expression would fail to take a
dependency if it referenced a hoisted `const` inferred as a primitive
value. We were incorrectly skipping primitve-typed operands when
determing scopes for merging in InferReactiveScopeVariables.

This was super tricky to debug, for posterity the trick is that Context
variables (StoreContext etc) are modeled just like a mutable object,
where assignment to the variable is equivalent to `object.value = ...`
and reading the variable is equivalent to `object.value` property
access. Comparing to an equivalent version of the repro case replaced
with an object and property read/writes showed that everything was
exactly right, except that InferReactiveScopeVariables wasn't merging
the scopes of the function and the context variable, which led me right
to the problematic line.

Closes #35122
This commit is contained in:
Joseph Savona
2025-12-05 11:29:06 -08:00
committed by GitHub
parent ad5971febd
commit 2cb08e65b3
3 changed files with 126 additions and 8 deletions

View File

@@ -389,14 +389,6 @@ export function findDisjointMutableValues(
*/
operand.identifier.mutableRange.start > 0
) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
if (operand.identifier.type.kind === 'Primitive') {
continue;
}
}
operands.push(operand.identifier);
}
}

View File

@@ -0,0 +1,94 @@
## Input
```javascript
import {useState} from 'react';
/**
* Repro for https://github.com/facebook/react/issues/35122
*
* InferReactiveScopeVariables was excluding primitive operands
* when considering operands for merging. We previously did not
* infer types for context variables (StoreContext etc), but later
* started inferring types in cases of `const` context variables,
* since the type cannot change.
*
* In this example, this meant that we skipped the `isExpired`
* operand of the onClick function expression when considering
* scopes to merge.
*/
function Test1() {
const [expire, setExpire] = useState(5);
const onClick = () => {
// Reference to isExpired prior to declaration
console.log('isExpired', isExpired);
};
const isExpired = expire === 0;
return <div onClick={onClick}>{expire}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Test1,
params: [{}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useState } from "react";
/**
* Repro for https://github.com/facebook/react/issues/35122
*
* InferReactiveScopeVariables was excluding primitive operands
* when considering operands for merging. We previously did not
* infer types for context variables (StoreContext etc), but later
* started inferring types in cases of `const` context variables,
* since the type cannot change.
*
* In this example, this meant that we skipped the `isExpired`
* operand of the onClick function expression when considering
* scopes to merge.
*/
function Test1() {
const $ = _c(5);
const [expire] = useState(5);
let onClick;
if ($[0] !== expire) {
onClick = () => {
console.log("isExpired", isExpired);
};
const isExpired = expire === 0;
$[0] = expire;
$[1] = onClick;
} else {
onClick = $[1];
}
let t0;
if ($[2] !== expire || $[3] !== onClick) {
t0 = <div onClick={onClick}>{expire}</div>;
$[2] = expire;
$[3] = onClick;
$[4] = t0;
} else {
t0 = $[4];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Test1,
params: [{}],
};
```
### Eval output
(kind: ok) <div>5</div>

View File

@@ -0,0 +1,32 @@
import {useState} from 'react';
/**
* Repro for https://github.com/facebook/react/issues/35122
*
* InferReactiveScopeVariables was excluding primitive operands
* when considering operands for merging. We previously did not
* infer types for context variables (StoreContext etc), but later
* started inferring types in cases of `const` context variables,
* since the type cannot change.
*
* In this example, this meant that we skipped the `isExpired`
* operand of the onClick function expression when considering
* scopes to merge.
*/
function Test1() {
const [expire, setExpire] = useState(5);
const onClick = () => {
// Reference to isExpired prior to declaration
console.log('isExpired', isExpired);
};
const isExpired = expire === 0;
return <div onClick={onClick}>{expire}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Test1,
params: [{}],
};