Compare commits

...

9 Commits

Author SHA1 Message Date
Mofei Zhang
e7a580fd29 refactor to use scope dependency
Summary:

Test Plan:

Reviewers:

Subscribers:

Tasks:

Tags:
2024-11-21 14:30:13 -05:00
Jordan Brown
d77ad3d0bb prettier 2024-11-19 12:01:17 -05:00
Jordan Brown
d99e393f82 update tests 2024-11-19 11:56:33 -05:00
Jordan Brown
cedc4b698b update tests 2024-11-19 11:53:21 -05:00
Jordan Brown
0481dddce8 prettier again lol 2024-11-19 11:53:21 -05:00
Jordan Brown
72b6f1ba56 No need to spread a filtered array 2024-11-19 11:53:21 -05:00
Jordan Brown
5d7cc78ded Address PR feedback + run prettier 2024-11-19 11:53:19 -05:00
Jordan Brown
3bd6f87106 First cut of inferring effect deps 2024-11-19 11:51:00 -05:00
Jordan Brown
469ae32692 Add env config to infer effect deps 2024-11-19 11:50:11 -05:00
25 changed files with 1124 additions and 0 deletions

View File

@@ -21,6 +21,7 @@ import {
lower,
mergeConsecutiveBlocks,
mergeOverlappingReactiveScopesHIR,
printHIR,
pruneUnusedLabelsHIR,
} from '../HIR';
import {
@@ -36,6 +37,7 @@ import {
inferReactivePlaces,
inferReferenceEffects,
inlineImmediatelyInvokedFunctionExpressions,
inferEffectDependencies,
} from '../Inference';
import {
constantPropagation,
@@ -354,6 +356,10 @@ function* runWithEnvironment(
value: hir,
});
if (env.config.inferEffectDependencies) {
inferEffectDependencies(env, hir);
}
if (env.config.inlineJsxTransform) {
inlineJsxTransform(hir, env.config.inlineJsxTransform);
yield log({

View File

@@ -233,6 +233,19 @@ const EnvironmentConfigSchema = z.object({
enableFunctionDependencyRewrite: z.boolean().default(true),
/**
* Enables inference of optional dependency chains. Without this flag
* a property chain such as `props?.items?.foo` will infer as a dep on
* just `props`. With this flag enabled, we'll infer that full path as
* the dependency.
*/
enableOptionalDependencies: z.boolean().default(true),
/**
* Enables inference and auto-insertion of effect dependencies. Still experimental.
*/
inferEffectDependencies: z.boolean().default(false),
/**
* Enables inlining ReactElement object literals in place of JSX
* An alternative to the standard JSX transform which replaces JSX with React's jsxProd() runtime

View File

@@ -0,0 +1,242 @@
import {CompilerError, SourceLocation} from '..';
import {
ArrayExpression,
Effect,
Environment,
FunctionExpression,
GeneratedSource,
HIRFunction,
IdentifierId,
Instruction,
isUseEffectHookType,
makeInstructionId,
TInstruction,
InstructionId,
ScopeId,
ReactiveScopeDependency,
Place,
ReactiveScopeDependencies,
} from '../HIR';
import {
createTemporaryPlace,
fixScopeAndIdentifierRanges,
markInstructionIds,
} from '../HIR/HIRBuilder';
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
/**
* Infers reactive dependencies captured by useEffect lambdas and adds them as
* a second argument to the useEffect call if no dependency array is provided.
*/
export function inferEffectDependencies(
env: Environment,
fn: HIRFunction,
): void {
let hasRewrite = false;
const fnExpressions = new Map<
IdentifierId,
TInstruction<FunctionExpression>
>();
const scopeInfos = new Map<
ScopeId,
{pruned: boolean; deps: ReactiveScopeDependencies; hasSingleInstr: boolean}
>();
/**
* When inserting LoadLocals, we need to retain the reactivity of the base
* identifier, as later passes e.g. PruneNonReactiveDeps take the reactivity of
* a base identifier as the "maximal" reactivity of all its references.
* Concretely,
* reactive(Identifier i) = Union_{reference of i}(reactive(reference))
*/
const reactiveIds = inferReactiveIdentifiers(fn);
for (const [, block] of fn.body.blocks) {
if (
block.terminal.kind === 'scope' ||
block.terminal.kind === 'pruned-scope'
) {
const scopeBlock = fn.body.blocks.get(block.terminal.block)!;
scopeInfos.set(block.terminal.scope.id, {
pruned: block.terminal.kind === 'pruned-scope',
deps: block.terminal.scope.dependencies,
hasSingleInstr:
scopeBlock.instructions.length === 1 &&
scopeBlock.terminal.kind === 'goto' &&
scopeBlock.terminal.block === block.terminal.fallthrough,
});
}
const rewriteInstrs = new Map<InstructionId, Array<Instruction>>();
for (const instr of block.instructions) {
const {value, lvalue} = instr;
if (value.kind === 'FunctionExpression') {
fnExpressions.set(
lvalue.identifier.id,
instr as TInstruction<FunctionExpression>,
);
} else if (
/*
* This check is not final. Right now we only look for useEffects without a dependency array.
* This is likely not how we will ship this feature, but it is good enough for us to make progress
* on the implementation and test it.
*/
value.kind === 'CallExpression' &&
isUseEffectHookType(value.callee.identifier) &&
value.args.length === 1 &&
value.args[0].kind === 'Identifier'
) {
const fnExpr = fnExpressions.get(value.args[0].identifier.id);
if (fnExpr != null) {
const scopeInfo =
fnExpr.lvalue.identifier.scope != null
? scopeInfos.get(fnExpr.lvalue.identifier.scope.id)
: null;
CompilerError.invariant(scopeInfo != null, {
reason: 'Expected function expression scope to exist',
loc: value.loc,
});
if (scopeInfo.pruned || !scopeInfo.hasSingleInstr) {
// TODO: retry pipeline that ensures effect function expressions
// are placed into their own scope
CompilerError.throwTodo({
reason:
'[InferEffectDependencies] Expected effect function to have non-pruned scope and its scope to have exactly one instruction',
loc: fnExpr.loc,
});
}
/**
* Step 1: write new instructions to insert a dependency array
*/
const effectDeps: Array<Place> = [];
const newInstructions: Array<Instruction> = [];
for (const dep of scopeInfo.deps) {
/**
* Invalid to prune non-reactive values in this pass, see the
* `infer-effect-deps/pruned-nonreactive-obj` fixture for an
* explanation
*/
const {place, instructions} = writeDependencyToInstructions(
dep,
reactiveIds.has(dep.identifier.id),
fn.env,
fnExpr.loc,
);
newInstructions.push(...instructions);
effectDeps.push(place);
}
const deps: ArrayExpression = {
kind: 'ArrayExpression',
elements: effectDeps,
loc: GeneratedSource,
};
const depsPlace = createTemporaryPlace(env, GeneratedSource);
depsPlace.effect = Effect.Read;
newInstructions.push({
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...depsPlace, effect: Effect.Mutate},
value: deps,
});
// Step 2: insert the deps array as an argument of the useEffect
value.args[1] = {...depsPlace, effect: Effect.Freeze};
rewriteInstrs.set(instr.id, newInstructions);
}
}
}
if (rewriteInstrs.size > 0) {
hasRewrite = true;
const newInstrs = [];
for (const instr of block.instructions) {
const newInstr = rewriteInstrs.get(instr.id);
if (newInstr != null) {
newInstrs.push(...newInstr, instr);
} else {
newInstrs.push(instr);
}
}
block.instructions = newInstrs;
}
}
if (hasRewrite) {
// Renumber instructions and fix scope ranges
markInstructionIds(fn.body);
fixScopeAndIdentifierRanges(fn.body);
}
}
function writeDependencyToInstructions(
dep: ReactiveScopeDependency,
reactive: boolean,
env: Environment,
loc: SourceLocation,
): {place: Place; instructions: Array<Instruction>} {
const instructions: Array<Instruction> = [];
let currValue = createTemporaryPlace(env, GeneratedSource);
currValue.reactive = reactive;
instructions.push({
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...currValue, effect: Effect.Mutate},
value: {
kind: 'LoadLocal',
place: {
kind: 'Identifier',
identifier: dep.identifier,
effect: Effect.Capture,
reactive,
loc: loc,
},
loc: loc,
},
});
for (const path of dep.path) {
if (path.optional) {
// TODO: instead of truncating optional paths, reuse
// instructions from hoisted dependencies block
break;
}
const nextValue = createTemporaryPlace(env, GeneratedSource);
nextValue.reactive = reactive;
instructions.push({
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...nextValue, effect: Effect.Mutate},
value: {
kind: 'PropertyLoad',
object: {...currValue, effect: Effect.Capture},
property: path.property,
loc: loc,
},
});
currValue = nextValue;
}
currValue.effect = Effect.Freeze;
return {place: currValue, instructions};
}
function inferReactiveIdentifiers(fn: HIRFunction): Set<IdentifierId> {
const reactiveIds: Set<IdentifierId> = new Set();
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
// no need to traverse into nested functions as
// 1. Their effects are recorded in `LoweredFunction.dependencies`
// 2. we don't mark `reactive` in these anyways
for (const place of eachInstructionOperand(instr)) {
if (place.reactive) {
reactiveIds.add(place.identifier.id);
}
}
}
for (const place of eachTerminalOperand(block.terminal)) {
if (place.reactive) {
reactiveIds.add(place.identifier.id);
}
}
}
return reactiveIds;
}

View File

@@ -11,3 +11,4 @@ export {inferMutableRanges} from './InferMutableRanges';
export {inferReactivePlaces} from './InferReactivePlaces';
export {default as inferReferenceEffects} from './InferReferenceEffects';
export {inlineImmediatelyInvokedFunctionExpressions} from './InlineImmediatelyInvokedFunctionExpressions';
export {inferEffectDependencies} from './InferEffectDependencies';

View File

@@ -0,0 +1,123 @@
## Input
```javascript
// @inferEffectDependencies
const moduleNonReactive = 0;
function Component({foo, bar}) {
const localNonreactive = 0;
const ref = useRef(0);
const localNonPrimitiveReactive = {
foo,
};
const localNonPrimitiveNonreactive = {};
useEffect(() => {
console.log(foo);
console.log(bar);
console.log(moduleNonReactive);
console.log(localNonreactive);
console.log(globalValue);
console.log(ref.current);
console.log(localNonPrimitiveReactive);
console.log(localNonPrimitiveNonreactive);
});
// Optional chains and property accesses
// TODO: we may be able to save bytes by omitting property accesses if the
// object of the member expression is already included in the inferred deps
useEffect(() => {
console.log(bar?.baz);
console.log(bar.qux);
});
function f() {
console.log(foo);
}
// No inferred dep array, the argument is not a lambda
useEffect(f);
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
const moduleNonReactive = 0;
function Component(t0) {
const $ = _c(12);
const { foo, bar } = t0;
const ref = useRef(0);
let t1;
if ($[0] !== foo) {
t1 = { foo };
$[0] = foo;
$[1] = t1;
} else {
t1 = $[1];
}
const localNonPrimitiveReactive = t1;
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = {};
$[2] = t2;
} else {
t2 = $[2];
}
const localNonPrimitiveNonreactive = t2;
let t3;
if ($[3] !== bar || $[4] !== foo || $[5] !== localNonPrimitiveReactive) {
t3 = () => {
console.log(foo);
console.log(bar);
console.log(moduleNonReactive);
console.log(0);
console.log(globalValue);
console.log(ref.current);
console.log(localNonPrimitiveReactive);
console.log(localNonPrimitiveNonreactive);
};
$[3] = bar;
$[4] = foo;
$[5] = localNonPrimitiveReactive;
$[6] = t3;
} else {
t3 = $[6];
}
useEffect(t3, [foo, bar, localNonPrimitiveReactive]);
let t4;
if ($[7] !== bar.baz || $[8] !== bar.qux) {
t4 = () => {
console.log(bar?.baz);
console.log(bar.qux);
};
$[7] = bar.baz;
$[8] = bar.qux;
$[9] = t4;
} else {
t4 = $[9];
}
useEffect(t4, [bar, bar.qux]);
let t5;
if ($[10] !== foo) {
t5 = function f() {
console.log(foo);
};
$[10] = foo;
$[11] = t5;
} else {
t5 = $[11];
}
const f = t5;
useEffect(f);
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,36 @@
// @inferEffectDependencies
const moduleNonReactive = 0;
function Component({foo, bar}) {
const localNonreactive = 0;
const ref = useRef(0);
const localNonPrimitiveReactive = {
foo,
};
const localNonPrimitiveNonreactive = {};
useEffect(() => {
console.log(foo);
console.log(bar);
console.log(moduleNonReactive);
console.log(localNonreactive);
console.log(globalValue);
console.log(ref.current);
console.log(localNonPrimitiveReactive);
console.log(localNonPrimitiveNonreactive);
});
// Optional chains and property accesses
// TODO: we may be able to save bytes by omitting property accesses if the
// object of the member expression is already included in the inferred deps
useEffect(() => {
console.log(bar?.baz);
console.log(bar.qux);
});
function f() {
console.log(foo);
}
// No inferred dep array, the argument is not a lambda
useEffect(f);
}

View File

@@ -0,0 +1,80 @@
## Input
```javascript
// @inferEffectDependencies
import {useEffect} from 'react';
import {makeObject_Primitives, print} from 'shared-runtime';
/**
* Note that `obj` is currently added to the effect dependency array, even
* though it's non-reactive due to memoization.
*
* This is a TODO in effect dependency inference. Note that we cannot simply
* filter out non-reactive effect dependencies, as some non-reactive (by data
* flow) values become reactive due to scope pruning. See the
* `infer-effect-deps/pruned-nonreactive-obj` fixture for why this matters.
*
* Realizing that this `useEffect` should have an empty dependency array
* requires effect dependency inference to be structured similarly to memo
* dependency inference.
* Pass 1: add all potential dependencies regardless of dataflow reactivity
* Pass 2: (todo) prune non-reactive dependencies
*
* Note that instruction reordering should significantly reduce scope pruning
*/
function NonReactiveDepInEffect() {
const obj = makeObject_Primitives();
useEffect(() => print(obj));
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
import { useEffect } from "react";
import { makeObject_Primitives, print } from "shared-runtime";
/**
* Note that `obj` is currently added to the effect dependency array, even
* though it's non-reactive due to memoization.
*
* This is a TODO in effect dependency inference. Note that we cannot simply
* filter out non-reactive effect dependencies, as some non-reactive (by data
* flow) values become reactive due to scope pruning. See the
* `infer-effect-deps/pruned-nonreactive-obj` fixture for why this matters.
*
* Realizing that this `useEffect` should have an empty dependency array
* requires effect dependency inference to be structured similarly to memo
* dependency inference.
* Pass 1: add all potential dependencies regardless of dataflow reactivity
* Pass 2: (todo) prune non-reactive dependencies
*
* Note that instruction reordering should significantly reduce scope pruning
*/
function NonReactiveDepInEffect() {
const $ = _c(2);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = makeObject_Primitives();
$[0] = t0;
} else {
t0 = $[0];
}
const obj = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => print(obj);
$[1] = t1;
} else {
t1 = $[1];
}
useEffect(t1, [obj]);
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,25 @@
// @inferEffectDependencies
import {useEffect} from 'react';
import {makeObject_Primitives, print} from 'shared-runtime';
/**
* Note that `obj` is currently added to the effect dependency array, even
* though it's non-reactive due to memoization.
*
* This is a TODO in effect dependency inference. Note that we cannot simply
* filter out non-reactive effect dependencies, as some non-reactive (by data
* flow) values become reactive due to scope pruning. See the
* `infer-effect-deps/pruned-nonreactive-obj` fixture for why this matters.
*
* Realizing that this `useEffect` should have an empty dependency array
* requires effect dependency inference to be structured similarly to memo
* dependency inference.
* Pass 1: add all potential dependencies regardless of dataflow reactivity
* Pass 2: (todo) prune non-reactive dependencies
*
* Note that instruction reordering should significantly reduce scope pruning
*/
function NonReactiveDepInEffect() {
const obj = makeObject_Primitives();
useEffect(() => print(obj));
}

View File

@@ -0,0 +1,51 @@
## Input
```javascript
// @inferEffectDependencies
import {useEffect, useRef} from 'react';
import {print} from 'shared-runtime';
/**
* Special case of `infer-effect-deps/nonreactive-dep`.
*
* We know that local `useRef` return values are stable, regardless of
* inferred memoization.
*/
function NonReactiveRefInEffect() {
const ref = useRef('initial value');
useEffect(() => print(ref.current));
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
import { useEffect, useRef } from "react";
import { print } from "shared-runtime";
/**
* Special case of `infer-effect-deps/nonreactive-dep`.
*
* We know that local `useRef` return values are stable, regardless of
* inferred memoization.
*/
function NonReactiveRefInEffect() {
const $ = _c(1);
const ref = useRef("initial value");
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = () => print(ref.current);
$[0] = t0;
} else {
t0 = $[0];
}
useEffect(t0, [ref]);
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,14 @@
// @inferEffectDependencies
import {useEffect, useRef} from 'react';
import {print} from 'shared-runtime';
/**
* Special case of `infer-effect-deps/nonreactive-dep`.
*
* We know that local `useRef` return values are stable, regardless of
* inferred memoization.
*/
function NonReactiveRefInEffect() {
const ref = useRef('initial value');
useEffect(() => print(ref.current));
}

View File

@@ -0,0 +1,46 @@
## Input
```javascript
// @inferEffectDependencies
import {useEffect} from 'react';
import {print} from 'shared-runtime';
/**
* This compiled output is technically incorrect but this is currently the same
* case as a bailout (an effect that overfires).
*
* To ensure an empty deps array is passed, we need special case
* `InferEffectDependencies` for outlined functions (likely easier) or run it
* before OutlineFunctions
*/
function OutlinedFunctionInEffect() {
useEffect(() => print('hello world!'));
}
```
## Code
```javascript
// @inferEffectDependencies
import { useEffect } from "react";
import { print } from "shared-runtime";
/**
* This compiled output is technically incorrect but this is currently the same
* case as a bailout (an effect that overfires).
*
* To ensure an empty deps array is passed, we need special case
* `InferEffectDependencies` for outlined functions (likely easier) or run it
* before OutlineFunctions
*/
function OutlinedFunctionInEffect() {
useEffect(_temp);
}
function _temp() {
return print("hello world!");
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,14 @@
// @inferEffectDependencies
import {useEffect} from 'react';
import {print} from 'shared-runtime';
/**
* This compiled output is technically incorrect but this is currently the same
* case as a bailout (an effect that overfires).
*
* To ensure an empty deps array is passed, we need special case
* `InferEffectDependencies` for outlined functions (likely easier) or run it
* before OutlineFunctions
*/
function OutlinedFunctionInEffect() {
useEffect(() => print('hello world!'));
}

View File

@@ -0,0 +1,119 @@
## Input
```javascript
// @inferEffectDependencies
import {useIdentity, mutate, makeObject} from 'shared-runtime';
import {useEffect} from 'react';
/**
* When a semantically non-reactive value has a pruned scope (i.e. the object
* identity becomes reactive, but the underlying value it represents should be
* constant), the compiler can choose to either
* - add it as a dependency (and rerun the effect)
* - not add it as a dependency
*
* We keep semantically non-reactive values in both memo block and effect
* dependency arrays to avoid versioning invariants e.g. `x !== y.aliasedX`.
* ```js
* function Component() {
* // obj is semantically non-reactive, but its memo scope is pruned due to
* // the interleaving hook call
* const obj = {};
* useHook();
* write(obj);
*
* const ref = useRef();
*
* // this effect needs to be rerun when obj's referential identity changes,
* // because it might alias obj to a useRef / mutable store.
* useEffect(() => ref.current = obj, ???);
*
* // in a custom hook (or child component), the user might expect versioning
* // invariants to hold
* useHook(ref, obj);
* }
*
* // defined elsewhere
* function useHook(someRef, obj) {
* useEffect(
* () => assert(someRef.current === obj),
* [someRef, obj]
* );
* }
* ```
*/
function PrunedNonReactive() {
const obj = makeObject();
useIdentity(null);
mutate(obj);
useEffect(() => print(obj.value));
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
import { useIdentity, mutate, makeObject } from "shared-runtime";
import { useEffect } from "react";
/**
* When a semantically non-reactive value has a pruned scope (i.e. the object
* identity becomes reactive, but the underlying value it represents should be
* constant), the compiler can choose to either
* - add it as a dependency (and rerun the effect)
* - not add it as a dependency
*
* We keep semantically non-reactive values in both memo block and effect
* dependency arrays to avoid versioning invariants e.g. `x !== y.aliasedX`.
* ```js
* function Component() {
* // obj is semantically non-reactive, but its memo scope is pruned due to
* // the interleaving hook call
* const obj = {};
* useHook();
* write(obj);
*
* const ref = useRef();
*
* // this effect needs to be rerun when obj's referential identity changes,
* // because it might alias obj to a useRef / mutable store.
* useEffect(() => ref.current = obj, ???);
*
* // in a custom hook (or child component), the user might expect versioning
* // invariants to hold
* useHook(ref, obj);
* }
*
* // defined elsewhere
* function useHook(someRef, obj) {
* useEffect(
* () => assert(someRef.current === obj),
* [someRef, obj]
* );
* }
* ```
*/
function PrunedNonReactive() {
const $ = _c(2);
const obj = makeObject();
useIdentity(null);
mutate(obj);
let t0;
if ($[0] !== obj.value) {
t0 = () => print(obj.value);
$[0] = obj.value;
$[1] = t0;
} else {
t0 = $[1];
}
useEffect(t0, [obj.value]);
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,48 @@
// @inferEffectDependencies
import {useIdentity, mutate, makeObject} from 'shared-runtime';
import {useEffect} from 'react';
/**
* When a semantically non-reactive value has a pruned scope (i.e. the object
* identity becomes reactive, but the underlying value it represents should be
* constant), the compiler can choose to either
* - add it as a dependency (and rerun the effect)
* - not add it as a dependency
*
* We keep semantically non-reactive values in both memo block and effect
* dependency arrays to avoid versioning invariants e.g. `x !== y.aliasedX`.
* ```js
* function Component() {
* // obj is semantically non-reactive, but its memo scope is pruned due to
* // the interleaving hook call
* const obj = {};
* useHook();
* write(obj);
*
* const ref = useRef();
*
* // this effect needs to be rerun when obj's referential identity changes,
* // because it might alias obj to a useRef / mutable store.
* useEffect(() => ref.current = obj, ???);
*
* // in a custom hook (or child component), the user might expect versioning
* // invariants to hold
* useHook(ref, obj);
* }
*
* // defined elsewhere
* function useHook(someRef, obj) {
* useEffect(
* () => assert(someRef.current === obj),
* [someRef, obj]
* );
* }
* ```
*/
function PrunedNonReactive() {
const obj = makeObject();
useIdentity(null);
mutate(obj);
useEffect(() => print(obj.value));
}

View File

@@ -0,0 +1,49 @@
## Input
```javascript
// @inferEffectDependencies
import {useEffect} from 'react';
import {print} from 'shared-runtime';
function ReactiveMemberExprMerge({propVal}) {
const obj = {a: {b: propVal}};
useEffect(() => print(obj.a, obj.a.b));
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
import { useEffect } from "react";
import { print } from "shared-runtime";
function ReactiveMemberExprMerge(t0) {
const $ = _c(4);
const { propVal } = t0;
let t1;
if ($[0] !== propVal) {
t1 = { a: { b: propVal } };
$[0] = propVal;
$[1] = t1;
} else {
t1 = $[1];
}
const obj = t1;
let t2;
if ($[2] !== obj.a) {
t2 = () => print(obj.a, obj.a.b);
$[2] = obj.a;
$[3] = t2;
} else {
t2 = $[3];
}
useEffect(t2, [obj.a]);
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,8 @@
// @inferEffectDependencies
import {useEffect} from 'react';
import {print} from 'shared-runtime';
function ReactiveMemberExprMerge({propVal}) {
const obj = {a: {b: propVal}};
useEffect(() => print(obj.a, obj.a.b));
}

View File

@@ -0,0 +1,49 @@
## Input
```javascript
// @inferEffectDependencies
import {useEffect} from 'react';
import {print} from 'shared-runtime';
function ReactiveMemberExpr({propVal}) {
const obj = {a: {b: propVal}};
useEffect(() => print(obj.a.b));
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
import { useEffect } from "react";
import { print } from "shared-runtime";
function ReactiveMemberExpr(t0) {
const $ = _c(4);
const { propVal } = t0;
let t1;
if ($[0] !== propVal) {
t1 = { a: { b: propVal } };
$[0] = propVal;
$[1] = t1;
} else {
t1 = $[1];
}
const obj = t1;
let t2;
if ($[2] !== obj.a.b) {
t2 = () => print(obj.a.b);
$[2] = obj.a.b;
$[3] = t2;
} else {
t2 = $[3];
}
useEffect(t2, [obj.a.b]);
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,8 @@
// @inferEffectDependencies
import {useEffect} from 'react';
import {print} from 'shared-runtime';
function ReactiveMemberExpr({propVal}) {
const obj = {a: {b: propVal}};
useEffect(() => print(obj.a.b));
}

View File

@@ -0,0 +1,60 @@
## Input
```javascript
// @inferEffectDependencies
import {useEffect} from 'react';
import {print} from 'shared-runtime';
// TODO: take optional chains as dependencies
function ReactiveMemberExpr({cond, propVal}) {
const obj = {a: cond ? {b: propVal} : null};
useEffect(() => print(obj.a?.b));
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
import { useEffect } from "react";
import { print } from "shared-runtime";
// TODO: take optional chains as dependencies
function ReactiveMemberExpr(t0) {
const $ = _c(7);
const { cond, propVal } = t0;
let t1;
if ($[0] !== cond || $[1] !== propVal) {
t1 = cond ? { b: propVal } : null;
$[0] = cond;
$[1] = propVal;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[3] !== t1) {
t2 = { a: t1 };
$[3] = t1;
$[4] = t2;
} else {
t2 = $[4];
}
const obj = t2;
let t3;
if ($[5] !== obj.a?.b) {
t3 = () => print(obj.a?.b);
$[5] = obj.a?.b;
$[6] = t3;
} else {
t3 = $[6];
}
useEffect(t3, [obj.a]);
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,9 @@
// @inferEffectDependencies
import {useEffect} from 'react';
import {print} from 'shared-runtime';
// TODO: take optional chains as dependencies
function ReactiveMemberExpr({cond, propVal}) {
const obj = {a: cond ? {b: propVal} : null};
useEffect(() => print(obj.a?.b));
}

View File

@@ -0,0 +1,49 @@
## Input
```javascript
// @inferEffectDependencies
import {useEffect} from 'react';
import {print} from 'shared-runtime';
function ReactiveVariable({propVal}) {
const arr = [propVal];
useEffect(() => print(arr));
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
import { useEffect } from "react";
import { print } from "shared-runtime";
function ReactiveVariable(t0) {
const $ = _c(4);
const { propVal } = t0;
let t1;
if ($[0] !== propVal) {
t1 = [propVal];
$[0] = propVal;
$[1] = t1;
} else {
t1 = $[1];
}
const arr = t1;
let t2;
if ($[2] !== arr) {
t2 = () => print(arr);
$[2] = arr;
$[3] = t2;
} else {
t2 = $[3];
}
useEffect(t2, [arr]);
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,8 @@
// @inferEffectDependencies
import {useEffect} from 'react';
import {print} from 'shared-runtime';
function ReactiveVariable({propVal}) {
const arr = [propVal];
useEffect(() => print(arr));
}

View File

@@ -0,0 +1,50 @@
## Input
```javascript
// @inferEffectDependencies
import * as React from 'react';
/**
* TODO: recognize import namespace
*/
function NonReactiveDepInEffect() {
const obj = makeObject_Primitives();
React.useEffect(() => print(obj));
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
import * as React from "react";
/**
* TODO: recognize import namespace
*/
function NonReactiveDepInEffect() {
const $ = _c(2);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = makeObject_Primitives();
$[0] = t0;
} else {
t0 = $[0];
}
const obj = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => print(obj);
$[1] = t1;
} else {
t1 = $[1];
}
React.useEffect(t1);
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,10 @@
// @inferEffectDependencies
import * as React from 'react';
/**
* TODO: recognize import namespace
*/
function NonReactiveDepInEffect() {
const obj = makeObject_Primitives();
React.useEffect(() => print(obj));
}

View File

@@ -174,6 +174,11 @@ function makePluginOptions(
.filter(s => s.length > 0);
}
let inferEffectDependencies = false;
if (firstLine.includes('@inferEffectDependencies')) {
inferEffectDependencies = true;
}
let logs: Array<{filename: string | null; event: LoggerEvent}> = [];
let logger: Logger | null = null;
if (firstLine.includes('@logger')) {
@@ -197,6 +202,7 @@ function makePluginOptions(
hookPattern,
validatePreserveExistingMemoizationGuarantees,
validateBlocklistedImports,
inferEffectDependencies,
},
compilationMode,
logger,