Compare commits
9 Commits
multi-form
...
mofeiZ-eff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7a580fd29 | ||
|
|
d77ad3d0bb | ||
|
|
d99e393f82 | ||
|
|
cedc4b698b | ||
|
|
0481dddce8 | ||
|
|
72b6f1ba56 | ||
|
|
5d7cc78ded | ||
|
|
3bd6f87106 | ||
|
|
469ae32692 |
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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!'));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
// @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
function ReactiveVariable({propVal}) {
|
||||
const arr = [propVal];
|
||||
useEffect(() => print(arr));
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
// @inferEffectDependencies
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* TODO: recognize import namespace
|
||||
*/
|
||||
function NonReactiveDepInEffect() {
|
||||
const obj = makeObject_Primitives();
|
||||
React.useEffect(() => print(obj));
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user