Compare commits

...

1 Commits

Author SHA1 Message Date
Joe Savona
bc50ab4bff [compiler] Add enableObjectIsComparison feature flag
Adds a feature flag that causes the compiler to emit `!Object.is(a, b)` instead of `a !== b` for dependency comparisons in memoized scopes. This matches the comparison semantics used by React's own hooks (useState, useMemo, etc.), correctly handling NaN values.
2026-02-24 12:56:40 -08:00
11 changed files with 367 additions and 6 deletions

View File

@@ -83,6 +83,8 @@ export class ProgramContext {
knownReferencedNames: Set<string> = new Set();
// generated imports
imports: Map<string, Map<string, NonLocalImportSpecifier>> = new Map();
// hoisted program-level constants
constants: Map<string, {uid: string; init: t.Expression}> = new Map();
constructor({
program,
@@ -182,6 +184,16 @@ export class ProgramContext {
this.knownReferencedNames.add(name);
}
addProgramConstant(name: string, init: () => t.Expression): string {
const existing = this.constants.get(name);
if (existing !== undefined) {
return existing.uid;
}
const uid = this.newUid(name);
this.constants.set(name, {uid, init: init()});
return uid;
}
assertGlobalBinding(
name: string,
localScope?: BabelScope,
@@ -302,6 +314,13 @@ export function addImportsToProgram(
}
}
}
for (const [, {uid, init}] of programContext.constants) {
stmts.push(
t.variableDeclaration('const', [
t.variableDeclarator(t.identifier(uid), init),
]),
);
}
path.unshiftContainer('body', stmts);
}

View File

@@ -502,6 +502,16 @@ export const EnvironmentConfigSchema = z.object({
* 3. Force update / external sync - should use useSyncExternalStore
*/
enableVerboseNoSetStateInEffect: z.boolean().default(false),
/**
* When enabled, the compiler emits `!Object.is(a, b)` instead of `a !== b`
* for dependency comparisons in memoized scopes. This matches the comparison
* semantics used by React's own hooks (useState, useMemo, etc.), which use
* Object.is. The main difference is in handling of NaN: `NaN !== NaN` is
* always true (causing memos to never be reused), while
* `Object.is(NaN, NaN)` is true (correctly treating NaN as unchanged).
*/
enableObjectIsComparison: z.boolean().default(false),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;

View File

@@ -182,8 +182,8 @@ export function codegenFunction(
const index = cx.synthesizeName('$i');
preface.push(
t.ifStatement(
t.binaryExpression(
'!==',
codegenNegatedComparison(
cx,
t.memberExpression(
t.identifier(cx.synthesizeName('$')),
t.numericLiteral(fastRefreshState.cacheIndex),
@@ -557,6 +557,23 @@ function codegenBlockNoReset(
return t.blockStatement(statements);
}
function codegenNegatedComparison(
cx: Context,
left: t.Expression,
right: t.Expression,
): t.Expression {
if (cx.env.config.enableObjectIsComparison) {
const isName = cx.env.programContext.addProgramConstant('is', () =>
t.memberExpression(t.identifier('Object'), t.identifier('is')),
);
return t.unaryExpression(
'!',
t.callExpression(t.identifier(isName), [left, right]),
);
}
return t.binaryExpression('!==', left, right);
}
function codegenReactiveScope(
cx: Context,
statements: Array<t.Statement>,
@@ -574,8 +591,8 @@ function codegenReactiveScope(
for (const dep of [...scope.dependencies].sort(compareScopeDependency)) {
const index = cx.nextCacheIndex;
const comparison = t.binaryExpression(
'!==',
const comparison = codegenNegatedComparison(
cx,
t.memberExpression(
t.identifier(cx.synthesizeName('$')),
t.numericLiteral(index),
@@ -720,8 +737,8 @@ function codegenReactiveScope(
const name: ValidIdentifierName = earlyReturnValue.value.name.value;
statements.push(
t.ifStatement(
t.binaryExpression(
'!==',
codegenNegatedComparison(
cx,
t.identifier(name),
t.callExpression(
t.memberExpression(t.identifier('Symbol'), t.identifier('for')),

View File

@@ -0,0 +1,56 @@
## Input
```javascript
// @enableObjectIsComparison
const is = null;
function Component(props) {
const x = [props.x];
console.log(is);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: 42}],
sequentialRenders: [{x: 42}, {x: 42}, {x: 3.14}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
const _is = Object.is; // @enableObjectIsComparison
const is = null;
function Component(props) {
const $ = _c(2);
let t0;
if (!_is($[0], props.x)) {
t0 = [props.x];
$[0] = props.x;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
console.log(is);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ x: 42 }],
sequentialRenders: [{ x: 42 }, { x: 42 }, { x: 3.14 }],
};
```
### Eval output
(kind: ok) [42]
[42]
[3.14]
logs: [null,null,null]

View File

@@ -0,0 +1,14 @@
// @enableObjectIsComparison
const is = null;
function Component(props) {
const x = [props.x];
console.log(is);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: 42}],
sequentialRenders: [{x: 42}, {x: 42}, {x: 3.14}],
};

View File

@@ -0,0 +1,91 @@
## Input
```javascript
// @enableObjectIsComparison
import {makeArray} from 'shared-runtime';
function Component(props) {
let x = [];
if (props.cond) {
x.push(props.a);
return x;
} else {
return makeArray(props.b);
}
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
sequentialRenders: [
{cond: true, a: 42},
{cond: true, a: 42},
{cond: false, b: 3.14},
{cond: false, b: 3.14},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
const is = Object.is; // @enableObjectIsComparison
import { makeArray } from "shared-runtime";
function Component(props) {
const $ = _c(6);
let t0;
if (!is($[0], props.a) || !is($[1], props.b) || !is($[2], props.cond)) {
t0 = Symbol.for("react.early_return_sentinel");
bb0: {
const x = [];
if (props.cond) {
x.push(props.a);
t0 = x;
break bb0;
} else {
let t1;
if (!is($[4], props.b)) {
t1 = makeArray(props.b);
$[4] = props.b;
$[5] = t1;
} else {
t1 = $[5];
}
t0 = t1;
break bb0;
}
}
$[0] = props.a;
$[1] = props.b;
$[2] = props.cond;
$[3] = t0;
} else {
t0 = $[3];
}
if (!is(t0, Symbol.for("react.early_return_sentinel"))) {
return t0;
}
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
sequentialRenders: [
{ cond: true, a: 42 },
{ cond: true, a: 42 },
{ cond: false, b: 3.14 },
{ cond: false, b: 3.14 },
],
};
```
### Eval output
(kind: ok) [42]
[42]
[3.14]
[3.14]

View File

@@ -0,0 +1,23 @@
// @enableObjectIsComparison
import {makeArray} from 'shared-runtime';
function Component(props) {
let x = [];
if (props.cond) {
x.push(props.a);
return x;
} else {
return makeArray(props.b);
}
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
sequentialRenders: [
{cond: true, a: 42},
{cond: true, a: 42},
{cond: false, b: 3.14},
{cond: false, b: 3.14},
],
};

View File

@@ -0,0 +1,60 @@
## Input
```javascript
// @enableObjectIsComparison @enableResetCacheOnSourceFileChanges
function Component(props) {
const x = [props.x];
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: 42}],
sequentialRenders: [{x: 42}, {x: 42}, {x: 3.14}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
const is = Object.is; // @enableObjectIsComparison @enableResetCacheOnSourceFileChanges
function Component(props) {
const $ = _c(3);
if (
!is(
$[0],
"eb2ec56d8fdd083c203a119ff37576dc8782330598640f725524102ae79e8b5c",
)
) {
for (let $i = 0; $i < 3; $i += 1) {
$[$i] = Symbol.for("react.memo_cache_sentinel");
}
$[0] = "eb2ec56d8fdd083c203a119ff37576dc8782330598640f725524102ae79e8b5c";
}
let t0;
if (!is($[1], props.x)) {
t0 = [props.x];
$[1] = props.x;
$[2] = t0;
} else {
t0 = $[2];
}
const x = t0;
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ x: 42 }],
sequentialRenders: [{ x: 42 }, { x: 42 }, { x: 3.14 }],
};
```
### Eval output
(kind: ok) [42]
[42]
[3.14]

View File

@@ -0,0 +1,11 @@
// @enableObjectIsComparison @enableResetCacheOnSourceFileChanges
function Component(props) {
const x = [props.x];
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: 42}],
sequentialRenders: [{x: 42}, {x: 42}, {x: 3.14}],
};

View File

@@ -0,0 +1,49 @@
## Input
```javascript
// @enableObjectIsComparison
function Component(props) {
const x = [props.x];
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: 42}],
sequentialRenders: [{x: 42}, {x: 42}, {x: 3.14}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
const is = Object.is; // @enableObjectIsComparison
function Component(props) {
const $ = _c(2);
let t0;
if (!is($[0], props.x)) {
t0 = [props.x];
$[0] = props.x;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ x: 42 }],
sequentialRenders: [{ x: 42 }, { x: 42 }, { x: 3.14 }],
};
```
### Eval output
(kind: ok) [42]
[42]
[3.14]

View File

@@ -0,0 +1,11 @@
// @enableObjectIsComparison
function Component(props) {
const x = [props.x];
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: 42}],
sequentialRenders: [{x: 42}, {x: 42}, {x: 3.14}],
};