mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[compiler] Context variables as dependencies
We previously didn't track context variables in the hoistable values sidemap of `propagateScopeDependencies`. This was overly conservative as we *do* track the mutable range of context variables, and it is safe to hoist accesses to context variables after their last direct / aliased maybe-assignment.
```js
function Component({value}) {
// start of mutable range for `x`
let x = DEFAULT;
const setX = () => x = value;
const aliasedSet = maybeAlias(setX);
maybeCall(aliasedSet);
// end of mutable range for `x`
// here, we should be able to take x (and property reads
// off of x) as dependencies
return <Jsx value={x} />
}
```
This commit is contained in:
@@ -840,6 +840,11 @@ export type LoadLocal = {
|
||||
place: Place;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
export type LoadContext = {
|
||||
kind: 'LoadContext';
|
||||
place: Place;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
/*
|
||||
* The value of a given instruction. Note that values are not recursive: complex
|
||||
@@ -852,11 +857,7 @@ export type LoadLocal = {
|
||||
|
||||
export type InstructionValue =
|
||||
| LoadLocal
|
||||
| {
|
||||
kind: 'LoadContext';
|
||||
place: Place;
|
||||
loc: SourceLocation;
|
||||
}
|
||||
| LoadContext
|
||||
| {
|
||||
kind: 'DeclareLocal';
|
||||
lvalue: LValue;
|
||||
|
||||
+48
-17
@@ -17,6 +17,11 @@ import {
|
||||
areEqualPaths,
|
||||
IdentifierId,
|
||||
Terminal,
|
||||
InstructionValue,
|
||||
LoadContext,
|
||||
TInstruction,
|
||||
FunctionExpression,
|
||||
ObjectMethod,
|
||||
} from './HIR';
|
||||
import {
|
||||
collectHoistablePropertyLoads,
|
||||
@@ -223,11 +228,25 @@ export function collectTemporariesSidemap(
|
||||
fn,
|
||||
usedOutsideDeclaringScope,
|
||||
temporaries,
|
||||
false,
|
||||
null,
|
||||
);
|
||||
return temporaries;
|
||||
}
|
||||
|
||||
function isLoadContextMutable(
|
||||
instrValue: InstructionValue,
|
||||
id: InstructionId,
|
||||
): instrValue is LoadContext {
|
||||
if (instrValue.kind === 'LoadContext') {
|
||||
CompilerError.invariant(instrValue.place.identifier.scope != null, {
|
||||
reason:
|
||||
'[PropagateScopeDependencies] Expected all context variables to be assigned a scope',
|
||||
loc: instrValue.loc,
|
||||
});
|
||||
return id >= instrValue.place.identifier.scope.range.end;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Recursive collect a sidemap of all `LoadLocal` and `PropertyLoads` with a
|
||||
* function and all nested functions.
|
||||
@@ -239,17 +258,21 @@ function collectTemporariesSidemapImpl(
|
||||
fn: HIRFunction,
|
||||
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
|
||||
temporaries: Map<IdentifierId, ReactiveScopeDependency>,
|
||||
isInnerFn: boolean,
|
||||
innerFnContext: {instrId: InstructionId} | null,
|
||||
): void {
|
||||
for (const [_, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
const {value, lvalue} = instr;
|
||||
for (const {value, lvalue, id: origInstrId} of block.instructions) {
|
||||
const instrId =
|
||||
innerFnContext != null ? innerFnContext.instrId : origInstrId;
|
||||
const usedOutside = usedOutsideDeclaringScope.has(
|
||||
lvalue.identifier.declarationId,
|
||||
);
|
||||
|
||||
if (value.kind === 'PropertyLoad' && !usedOutside) {
|
||||
if (!isInnerFn || temporaries.has(value.object.identifier.id)) {
|
||||
if (
|
||||
innerFnContext == null ||
|
||||
temporaries.has(value.object.identifier.id)
|
||||
) {
|
||||
/**
|
||||
* All dependencies of a inner / nested function must have a base
|
||||
* identifier from the outermost component / hook. This is because the
|
||||
@@ -265,13 +288,13 @@ function collectTemporariesSidemapImpl(
|
||||
temporaries.set(lvalue.identifier.id, property);
|
||||
}
|
||||
} else if (
|
||||
value.kind === 'LoadLocal' &&
|
||||
(value.kind === 'LoadLocal' || isLoadContextMutable(value, instrId)) &&
|
||||
lvalue.identifier.name == null &&
|
||||
value.place.identifier.name !== null &&
|
||||
!usedOutside
|
||||
) {
|
||||
if (
|
||||
!isInnerFn ||
|
||||
innerFnContext == null ||
|
||||
fn.context.some(
|
||||
context => context.identifier.id === value.place.identifier.id,
|
||||
)
|
||||
@@ -289,7 +312,7 @@ function collectTemporariesSidemapImpl(
|
||||
value.loweredFunc.func,
|
||||
usedOutsideDeclaringScope,
|
||||
temporaries,
|
||||
true,
|
||||
innerFnContext ?? {instrId},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -364,7 +387,7 @@ class Context {
|
||||
* Tracks the traversal state. See Context.declare for explanation of why this
|
||||
* is needed.
|
||||
*/
|
||||
inInnerFn: boolean = false;
|
||||
#innerFnContext: {outerInstrId: InstructionId} | null = null;
|
||||
|
||||
constructor(
|
||||
temporariesUsedOutsideScope: ReadonlySet<DeclarationId>,
|
||||
@@ -434,7 +457,7 @@ class Context {
|
||||
* by root identifier mutable ranges).
|
||||
*/
|
||||
declare(identifier: Identifier, decl: Decl): void {
|
||||
if (this.inInnerFn) return;
|
||||
if (this.#innerFnContext != null) return;
|
||||
if (!this.#declarations.has(identifier.declarationId)) {
|
||||
this.#declarations.set(identifier.declarationId, decl);
|
||||
}
|
||||
@@ -577,11 +600,14 @@ class Context {
|
||||
currentScope.reassignments.add(place.identifier);
|
||||
}
|
||||
}
|
||||
enterInnerFn<T>(cb: () => T): T {
|
||||
const wasInInnerFn = this.inInnerFn;
|
||||
this.inInnerFn = true;
|
||||
enterInnerFn<T>(
|
||||
innerFn: TInstruction<FunctionExpression> | TInstruction<ObjectMethod>,
|
||||
cb: () => T,
|
||||
): T {
|
||||
const prevContext = this.#innerFnContext;
|
||||
this.#innerFnContext = this.#innerFnContext ?? {outerInstrId: innerFn.id};
|
||||
const result = cb();
|
||||
this.inInnerFn = wasInInnerFn;
|
||||
this.#innerFnContext = prevContext;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -724,9 +750,14 @@ function collectDependencies(
|
||||
* Recursively visit the inner function to extract dependencies there
|
||||
*/
|
||||
const innerFn = instr.value.loweredFunc.func;
|
||||
context.enterInnerFn(() => {
|
||||
handleFunction(innerFn);
|
||||
});
|
||||
context.enterInnerFn(
|
||||
instr as
|
||||
| TInstruction<FunctionExpression>
|
||||
| TInstruction<ObjectMethod>,
|
||||
() => {
|
||||
handleFunction(innerFn);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
handleInstruction(instr, context);
|
||||
}
|
||||
|
||||
+8
-10
@@ -58,18 +58,16 @@ function Foo(t0) {
|
||||
bar = $[1];
|
||||
result = $[2];
|
||||
}
|
||||
|
||||
const t1 = bar;
|
||||
let t2;
|
||||
if ($[3] !== result || $[4] !== t1) {
|
||||
t2 = <Stringify result={result} fn={t1} shouldInvokeFns={true} />;
|
||||
$[3] = result;
|
||||
$[4] = t1;
|
||||
$[5] = t2;
|
||||
let t1;
|
||||
if ($[3] !== bar || $[4] !== result) {
|
||||
t1 = <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
|
||||
$[3] = bar;
|
||||
$[4] = result;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
t1 = $[5];
|
||||
}
|
||||
return t2;
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
+7
-8
@@ -43,16 +43,15 @@ function Component(props) {
|
||||
} else {
|
||||
x = $[1];
|
||||
}
|
||||
const t0 = x;
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = { x: t0 };
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
let t0;
|
||||
if ($[2] !== x) {
|
||||
t0 = { x };
|
||||
$[2] = x;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t0 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
+7
-8
@@ -42,16 +42,15 @@ function Component(props) {
|
||||
} else {
|
||||
x = $[1];
|
||||
}
|
||||
const t0 = x;
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = <div>{t0}</div>;
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
let t0;
|
||||
if ($[2] !== x) {
|
||||
t0 = <div>{x}</div>;
|
||||
$[2] = x;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t0 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
+7
-8
@@ -43,16 +43,15 @@ function Component(props) {
|
||||
} else {
|
||||
x = $[1];
|
||||
}
|
||||
const t0 = x;
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = { x: t0 };
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
let t0;
|
||||
if ($[2] !== x) {
|
||||
t0 = { x };
|
||||
$[2] = x;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t0 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
+7
-8
@@ -42,16 +42,15 @@ function Component(props) {
|
||||
} else {
|
||||
x = $[1];
|
||||
}
|
||||
const t0 = x;
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = { x: t0 };
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
let t0;
|
||||
if ($[2] !== x) {
|
||||
t0 = { x };
|
||||
$[2] = x;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t0 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
+7
-9
@@ -33,17 +33,15 @@ function f(a) {
|
||||
} else {
|
||||
x = $[1];
|
||||
}
|
||||
|
||||
const t0 = x;
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = <div x={t0} />;
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
let t0;
|
||||
if ($[2] !== x) {
|
||||
t0 = <div x={x} />;
|
||||
$[2] = x;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t0 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useCallback} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* TODO: we're currently bailing out because `contextVar` is a context variable
|
||||
* and not recorded into the PropagateScopeDeps LoadLocal / PropertyLoad
|
||||
* sidemap. Previously, we were able to avoid this as `BuildHIR` hoisted
|
||||
* `LoadContext` and `PropertyLoad` instructions into the outer function, which
|
||||
* we took as eligible dependencies.
|
||||
*
|
||||
* One solution is to simply record `LoadContext` identifiers into the
|
||||
* temporaries sidemap when the instruction occurs *after* the context
|
||||
* variable's mutable range.
|
||||
*/
|
||||
function Foo(props) {
|
||||
let contextVar;
|
||||
if (props.cond) {
|
||||
contextVar = {val: 2};
|
||||
} else {
|
||||
contextVar = {};
|
||||
}
|
||||
|
||||
const cb = useCallback(() => [contextVar.val], [contextVar.val]);
|
||||
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{cond: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
22 | }
|
||||
23 |
|
||||
> 24 | const cb = useCallback(() => [contextVar.val], [contextVar.val]);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (24:24)
|
||||
25 |
|
||||
26 | return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
27 | }
|
||||
```
|
||||
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useCallback} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* TODO: we're currently bailing out because `contextVar` is a context variable
|
||||
* and not recorded into the PropagateScopeDeps LoadLocal / PropertyLoad
|
||||
* sidemap. Previously, we were able to avoid this as `BuildHIR` hoisted
|
||||
* `LoadContext` and `PropertyLoad` instructions into the outer function, which
|
||||
* we took as eligible dependencies.
|
||||
*
|
||||
* One solution is to simply record `LoadContext` identifiers into the
|
||||
* temporaries sidemap when the instruction occurs *after* the context
|
||||
* variable's mutable range.
|
||||
*/
|
||||
function Foo(props) {
|
||||
let contextVar;
|
||||
if (props.cond) {
|
||||
contextVar = {val: 2};
|
||||
} else {
|
||||
contextVar = {};
|
||||
}
|
||||
|
||||
const cb = useCallback(() => [contextVar.val], [contextVar.val]);
|
||||
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{cond: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
|
||||
import { useCallback } from "react";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* TODO: we're currently bailing out because `contextVar` is a context variable
|
||||
* and not recorded into the PropagateScopeDeps LoadLocal / PropertyLoad
|
||||
* sidemap. Previously, we were able to avoid this as `BuildHIR` hoisted
|
||||
* `LoadContext` and `PropertyLoad` instructions into the outer function, which
|
||||
* we took as eligible dependencies.
|
||||
*
|
||||
* One solution is to simply record `LoadContext` identifiers into the
|
||||
* temporaries sidemap when the instruction occurs *after* the context
|
||||
* variable's mutable range.
|
||||
*/
|
||||
function Foo(props) {
|
||||
const $ = _c(6);
|
||||
let contextVar;
|
||||
if ($[0] !== props.cond) {
|
||||
if (props.cond) {
|
||||
contextVar = { val: 2 };
|
||||
} else {
|
||||
contextVar = {};
|
||||
}
|
||||
$[0] = props.cond;
|
||||
$[1] = contextVar;
|
||||
} else {
|
||||
contextVar = $[1];
|
||||
}
|
||||
let t0;
|
||||
if ($[2] !== contextVar.val) {
|
||||
t0 = () => [contextVar.val];
|
||||
$[2] = contextVar.val;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t0 = $[3];
|
||||
}
|
||||
contextVar;
|
||||
const cb = t0;
|
||||
let t1;
|
||||
if ($[4] !== cb) {
|
||||
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
$[4] = cb;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{ cond: true }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"cb":{"kind":"Function","result":[2]},"shouldInvokeFns":true}</div>
|
||||
+7
-8
@@ -44,16 +44,15 @@ function useFoo(arr1, arr2) {
|
||||
y = $[2];
|
||||
}
|
||||
let t0;
|
||||
const t1 = y;
|
||||
let t2;
|
||||
if ($[3] !== t1) {
|
||||
t2 = { y: t1 };
|
||||
$[3] = t1;
|
||||
$[4] = t2;
|
||||
let t1;
|
||||
if ($[3] !== y) {
|
||||
t1 = { y };
|
||||
$[3] = y;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
t1 = $[4];
|
||||
}
|
||||
t0 = t2;
|
||||
t0 = t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
|
||||
+7
-9
@@ -36,17 +36,15 @@ function HomeDiscoStoreItemTileRating(props) {
|
||||
} else {
|
||||
count = $[1];
|
||||
}
|
||||
|
||||
const t0 = count;
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = <Text>{t0}</Text>;
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
let t0;
|
||||
if ($[2] !== count) {
|
||||
t0 = <Text>{count}</Text>;
|
||||
$[2] = count;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t0 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
+7
-9
@@ -67,17 +67,15 @@ function Component(props) {
|
||||
} else {
|
||||
x = $[1];
|
||||
}
|
||||
|
||||
const t0 = x;
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = [t0];
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
let t0;
|
||||
if ($[2] !== x) {
|
||||
t0 = [x];
|
||||
$[2] = x;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t0 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {throwErrorWithMessage, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Context variables are local variables that (1) have at least one reassignment
|
||||
* and (2) are captured into a function expression. These have a known mutable
|
||||
* range: from first declaration / assignment to the last direct or aliased,
|
||||
* mutable reference.
|
||||
*
|
||||
* This fixture validates that forget can take granular dependencies on context
|
||||
* variables when the reference to a context var happens *after* the end of its
|
||||
* mutable range.
|
||||
*/
|
||||
function Component({cond, a}) {
|
||||
let contextVar;
|
||||
if (cond) {
|
||||
contextVar = {val: a};
|
||||
} else {
|
||||
contextVar = {};
|
||||
throwErrorWithMessage('');
|
||||
}
|
||||
const cb = {cb: () => contextVar.val * 4};
|
||||
|
||||
/**
|
||||
* manually specify input to avoid adding a `PropertyLoad` from contextVar,
|
||||
* which might affect hoistable-objects analysis.
|
||||
*/
|
||||
return (
|
||||
<ValidateMemoization
|
||||
inputs={[cond ? a : undefined]}
|
||||
output={cb}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{cond: false, a: undefined}],
|
||||
sequentialRenders: [
|
||||
{cond: true, a: 2},
|
||||
{cond: true, a: 2},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { throwErrorWithMessage, ValidateMemoization } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Context variables are local variables that (1) have at least one reassignment
|
||||
* and (2) are captured into a function expression. These have a known mutable
|
||||
* range: from first declaration / assignment to the last direct or aliased,
|
||||
* mutable reference.
|
||||
*
|
||||
* This fixture validates that forget can take granular dependencies on context
|
||||
* variables when the reference to a context var happens *after* the end of its
|
||||
* mutable range.
|
||||
*/
|
||||
function Component(t0) {
|
||||
const $ = _c(10);
|
||||
const { cond, a } = t0;
|
||||
let contextVar;
|
||||
if ($[0] !== a || $[1] !== cond) {
|
||||
if (cond) {
|
||||
contextVar = { val: a };
|
||||
} else {
|
||||
contextVar = {};
|
||||
throwErrorWithMessage("");
|
||||
}
|
||||
$[0] = a;
|
||||
$[1] = cond;
|
||||
$[2] = contextVar;
|
||||
} else {
|
||||
contextVar = $[2];
|
||||
}
|
||||
let t1;
|
||||
if ($[3] !== contextVar.val) {
|
||||
t1 = { cb: () => contextVar.val * 4 };
|
||||
$[3] = contextVar.val;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
const cb = t1;
|
||||
|
||||
const t2 = cond ? a : undefined;
|
||||
let t3;
|
||||
if ($[5] !== t2) {
|
||||
t3 = [t2];
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
let t4;
|
||||
if ($[7] !== cb || $[8] !== t3) {
|
||||
t4 = (
|
||||
<ValidateMemoization inputs={t3} output={cb} onlyCheckCompiled={true} />
|
||||
);
|
||||
$[7] = cb;
|
||||
$[8] = t3;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ cond: false, a: undefined }],
|
||||
sequentialRenders: [
|
||||
{ cond: true, a: 2 },
|
||||
{ cond: true, a: 2 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[2],"output":{"cb":"[[ function params=0 ]]"}}</div>
|
||||
<div>{"inputs":[2],"output":{"cb":"[[ function params=0 ]]"}}</div>
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import {throwErrorWithMessage, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Context variables are local variables that (1) have at least one reassignment
|
||||
* and (2) are captured into a function expression. These have a known mutable
|
||||
* range: from first declaration / assignment to the last direct or aliased,
|
||||
* mutable reference.
|
||||
*
|
||||
* This fixture validates that forget can take granular dependencies on context
|
||||
* variables when the reference to a context var happens *after* the end of its
|
||||
* mutable range.
|
||||
*/
|
||||
function Component({cond, a}) {
|
||||
let contextVar;
|
||||
if (cond) {
|
||||
contextVar = {val: a};
|
||||
} else {
|
||||
contextVar = {};
|
||||
throwErrorWithMessage('');
|
||||
}
|
||||
const cb = {cb: () => contextVar.val * 4};
|
||||
|
||||
/**
|
||||
* manually specify input to avoid adding a `PropertyLoad` from contextVar,
|
||||
* which might affect hoistable-objects analysis.
|
||||
*/
|
||||
return (
|
||||
<ValidateMemoization
|
||||
inputs={[cond ? a : undefined]}
|
||||
output={cb}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{cond: false, a: undefined}],
|
||||
sequentialRenders: [
|
||||
{cond: true, a: 2},
|
||||
{cond: true, a: 2},
|
||||
],
|
||||
};
|
||||
+7
-9
@@ -35,17 +35,15 @@ function HomeDiscoStoreItemTileRating(props) {
|
||||
} else {
|
||||
count = $[1];
|
||||
}
|
||||
|
||||
const t0 = count;
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = <Text>{t0}</Text>;
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
let t0;
|
||||
if ($[2] !== count) {
|
||||
t0 = <Text>{count}</Text>;
|
||||
$[2] = count;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t0 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
+20
-22
@@ -88,36 +88,34 @@ function Inner(props) {
|
||||
input;
|
||||
input;
|
||||
let t0;
|
||||
const t1 = input;
|
||||
let t2;
|
||||
if ($[0] !== t1) {
|
||||
t2 = [t1];
|
||||
$[0] = t1;
|
||||
$[1] = t2;
|
||||
let t1;
|
||||
if ($[0] !== input) {
|
||||
t1 = [input];
|
||||
$[0] = input;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
t1 = $[1];
|
||||
}
|
||||
t0 = t2;
|
||||
t0 = t1;
|
||||
const output = t0;
|
||||
const t3 = input;
|
||||
let t4;
|
||||
if ($[2] !== t3) {
|
||||
t4 = [t3];
|
||||
$[2] = t3;
|
||||
$[3] = t4;
|
||||
let t2;
|
||||
if ($[2] !== input) {
|
||||
t2 = [input];
|
||||
$[2] = input;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t4 = $[3];
|
||||
t2 = $[3];
|
||||
}
|
||||
let t5;
|
||||
if ($[4] !== output || $[5] !== t4) {
|
||||
t5 = <ValidateMemoization inputs={t4} output={output} />;
|
||||
let t3;
|
||||
if ($[4] !== output || $[5] !== t2) {
|
||||
t3 = <ValidateMemoization inputs={t2} output={output} />;
|
||||
$[4] = output;
|
||||
$[5] = t4;
|
||||
$[6] = t5;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t5 = $[6];
|
||||
t3 = $[6];
|
||||
}
|
||||
return t5;
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -32,7 +32,15 @@ export function runSprout(
|
||||
originalCode: string,
|
||||
forgetCode: string,
|
||||
): SproutResult {
|
||||
const forgetResult = doEval(forgetCode);
|
||||
let forgetResult;
|
||||
try {
|
||||
(globalThis as any).__SNAP_EVALUATOR_MODE = 'forget';
|
||||
forgetResult = doEval(forgetCode);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
} finally {
|
||||
(globalThis as any).__SNAP_EVALUATOR_MODE = undefined;
|
||||
}
|
||||
if (forgetResult.kind === 'UnexpectedError') {
|
||||
return makeError('Unexpected error in Forget runner', forgetResult.value);
|
||||
}
|
||||
|
||||
@@ -259,26 +259,35 @@ export function Throw() {
|
||||
|
||||
export function ValidateMemoization({
|
||||
inputs,
|
||||
output,
|
||||
output: rawOutput,
|
||||
onlyCheckCompiled = false,
|
||||
}: {
|
||||
inputs: Array<any>;
|
||||
output: any;
|
||||
onlyCheckCompiled: boolean;
|
||||
}): React.ReactElement {
|
||||
'use no forget';
|
||||
// Wrap rawOutput as it might be a function, which useState would invoke.
|
||||
const output = {value: rawOutput};
|
||||
const [previousInputs, setPreviousInputs] = React.useState(inputs);
|
||||
const [previousOutput, setPreviousOutput] = React.useState(output);
|
||||
if (
|
||||
inputs.length !== previousInputs.length ||
|
||||
inputs.some((item, i) => item !== previousInputs[i])
|
||||
onlyCheckCompiled &&
|
||||
(globalThis as any).__SNAP_EVALUATOR_MODE === 'forget'
|
||||
) {
|
||||
// Some input changed, we expect the output to change
|
||||
setPreviousInputs(inputs);
|
||||
setPreviousOutput(output);
|
||||
} else if (output !== previousOutput) {
|
||||
// Else output should be stable
|
||||
throw new Error('Output identity changed but inputs did not');
|
||||
if (
|
||||
inputs.length !== previousInputs.length ||
|
||||
inputs.some((item, i) => item !== previousInputs[i])
|
||||
) {
|
||||
// Some input changed, we expect the output to change
|
||||
setPreviousInputs(inputs);
|
||||
setPreviousOutput(output);
|
||||
} else if (output.value !== previousOutput.value) {
|
||||
// Else output should be stable
|
||||
throw new Error('Output identity changed but inputs did not');
|
||||
}
|
||||
}
|
||||
return React.createElement(Stringify, {inputs, output});
|
||||
return React.createElement(Stringify, {inputs, output: rawOutput});
|
||||
}
|
||||
|
||||
export function createHookWrapper<TProps, TRet>(
|
||||
|
||||
Reference in New Issue
Block a user