[rhir] Refactor ReactiveScopeDependency, conditional dependencies (2/2)

--- 

See comment block in `PropagateScopeDependencies` and added test case 
`reduce-reactive-conditional-dependencies` for correctness properties / 
dependency merging logic.
This commit is contained in:
Mofei Zhang
2023-02-28 16:35:59 -05:00
parent 4005f862bd
commit 6b129b59ed
22 changed files with 832 additions and 214 deletions
@@ -22,9 +22,7 @@ import {
ReactiveValue,
} from "../HIR/HIR";
import { eachInstructionValueOperand } from "../HIR/visitors";
import { todoInvariant } from "../Utils/todo";
import { assertExhaustive } from "../Utils/utils";
import { eachReactiveValueOperand } from "./visitors";
/**
* Infers the dependencies of each scope to include variables whose values
@@ -60,10 +58,10 @@ type Scopes = Array<ReactiveScope>;
// TODO(@mofeiZ): remove once we replace Context.#dependencies, #properties with tree
// representation
function areDependenciesEqual(
dep1: ReactiveScopeDependency,
dep2: ReactiveScopeDependency
dep1: ReactiveScopeDependencyInfo,
dep2: ReactiveScopeDependencyInfo
): boolean {
if (dep1.identifier.id !== dep2.identifier.id) {
if (dep1.identifier.id !== dep2.identifier.id || dep1.cond !== dep2.cond) {
return false;
}
const dep1Path = dep1.path;
@@ -75,7 +73,7 @@ function areDependenciesEqual(
} else if (
dep1Path === null ||
dep2Path === null ||
dep2Path.length != dep1Path.length
dep2Path.length !== dep1Path.length
) {
return false;
}
@@ -85,6 +83,8 @@ function areDependenciesEqual(
});
}
type ReactiveScopeDependencyInfo = ReactiveScopeDependency & { cond: boolean };
/**
* Enum representing the access type of single property on a parent object.
* We distinguish on two independent axes:
@@ -97,23 +97,59 @@ function areDependenciesEqual(
* - Dependency: this property is read as a dependency and we must track changes
* to it for correctness.
*
* ```javascript
* // props.a is a dependency here and must be tracked
* deps: {props.a, props.a.b} ---> minimalDeps: {props.a}
* // props.a is just an access here and does not need to be tracked
* deps: {props.a.b} ---> minimalDeps: {props.a.b}
* ```
*/
enum PropertyAccessType {
ConditionalAccess = "ConditionalAccess",
UnconditionalAccess = "UnconditionalAccess",
ConditionalDependency = "ConditionalDependency",
UnconditionalDependency = "UnconditionalDependency",
}
function isUnconditional(access: PropertyAccessType) {
return (
access === PropertyAccessType.UnconditionalAccess ||
access === PropertyAccessType.UnconditionalDependency
);
}
function isDependency(access: PropertyAccessType) {
return (
access === PropertyAccessType.ConditionalDependency ||
access === PropertyAccessType.UnconditionalDependency
);
}
function merge(
access1: PropertyAccessType,
access2: PropertyAccessType
): PropertyAccessType {
if (
access1 === PropertyAccessType.UnconditionalDependency ||
access2 === PropertyAccessType.UnconditionalDependency
) {
return PropertyAccessType.UnconditionalDependency;
const resultIsUnconditional =
isUnconditional(access1) || isUnconditional(access2);
const resultIsDependency = isDependency(access1) || isDependency(access2);
// Straightforward merge.
// This can be represented as bitwise OR, but is written out for readability
//
// Observe that `UnconditionalAccess | ConditionalDependency` produces an
// unconditionally accessed conditional dependency. We currently use these
// as we use unconditional dependencies. (i.e. to codegen change variables)
if (resultIsUnconditional) {
if (resultIsDependency) {
return PropertyAccessType.UnconditionalDependency;
} else {
return PropertyAccessType.UnconditionalAccess;
}
} else {
return PropertyAccessType.UnconditionalAccess;
if (resultIsDependency) {
return PropertyAccessType.ConditionalDependency;
} else {
return PropertyAccessType.ConditionalAccess;
}
}
}
@@ -134,12 +170,24 @@ const promoteUncondResult = [
},
];
const promoteCondResult = [
{
relativePath: [],
accessType: PropertyAccessType.ConditionalDependency,
},
];
/**
* Recursively calculates minimal dependencies in a subtree.
* @param dep DependencyNode representing a dependency subtree.
* @returns a minimal list of dependencies in this subtree.
*/
function deriveMinimalDependenciesInSubtree(
dep: DependencyNode
): Array<ReduceResultNode> {
const results: Array<ReduceResultNode> = [];
for (const [childName, childNode] of dep.properties) {
const reduceResult = deriveMinimalDependenciesInSubtree(childNode).map(
const childResult = deriveMinimalDependenciesInSubtree(childNode).map(
({ relativePath, accessType }) => {
return {
relativePath: [childName, ...relativePath],
@@ -147,7 +195,7 @@ function deriveMinimalDependenciesInSubtree(
};
}
);
results.push(...reduceResult);
results.push(...childResult);
}
switch (dep.accessType) {
@@ -155,13 +203,42 @@ function deriveMinimalDependenciesInSubtree(
return promoteUncondResult;
}
case PropertyAccessType.UnconditionalAccess: {
// all children are unconditional dependencies, return them to preserve granularity
return results;
if (
results.every(
({ accessType }) =>
accessType === PropertyAccessType.UnconditionalDependency
)
) {
// all children are unconditional dependencies, return them to preserve granularity
return results;
} else {
// at least one child is accessed conditionally, so this node needs to be promoted to
// unconditional dependency
return promoteUncondResult;
}
}
case PropertyAccessType.ConditionalAccess:
case PropertyAccessType.ConditionalDependency: {
if (
results.every(
({ accessType }) =>
accessType === PropertyAccessType.ConditionalDependency
)
) {
// No children are accessed unconditionally, so we cannot promote this node to
// unconditional access.
// Truncate results of child nodes here, since we shouldn't access them anyways
return promoteCondResult;
} else {
// at least one child is accessed unconditionally, so this node can be promoted to
// unconditional dependency
return promoteUncondResult;
}
}
default: {
todoInvariant(
false,
"[PropgateScopeDependencies] Handle conditional dependencies."
assertExhaustive(
dep.accessType,
"[PropgateScopeDependencies] Unhandled access type!"
);
}
}
@@ -193,7 +270,7 @@ function deriveMinimalDependenciesInSubtree(
// TODO(@mofeiZ): change once we replace Context.#dependencies, #properties with tree
// representation
function deriveMinimalDependencies(
initialDeps: Set<ReactiveScopeDependency>
initialDeps: Set<ReactiveScopeDependencyInfo>
): Set<ReactiveScopeDependency> {
const depRoots = new Map<IdentifierId, [Identifier, DependencyNode]>();
@@ -209,9 +286,13 @@ function deriveMinimalDependencies(
depRoots.set(dep.identifier.id, [dep.identifier, root]);
}
let currNode: DependencyNode = root;
// TODO(@mofeiZ) add conditional access/dependencies here
const accessType = PropertyAccessType.UnconditionalAccess;
const depType = PropertyAccessType.UnconditionalDependency;
const accessType = dep.cond
? PropertyAccessType.ConditionalAccess
: PropertyAccessType.UnconditionalAccess;
const depType = dep.cond
? PropertyAccessType.ConditionalDependency
: PropertyAccessType.UnconditionalDependency;
for (const property of path) {
// all properties read 'on the way' to a dependency are marked as 'access'
@@ -256,20 +337,49 @@ function deriveMinimalDependencies(
class Context {
#declarations: DeclMap = new Map();
#reassignments: Map<Identifier, Decl> = new Map();
#dependencies: Set<ReactiveScopeDependency> = new Set();
#properties: Map<Identifier, ReactiveScopeDependency> = new Map();
#dependencies: Set<ReactiveScopeDependencyInfo> = new Set();
#properties: Map<Identifier, ReactiveScopeDependencyInfo> = new Map();
#temporaries: Map<Identifier, Place> = new Map();
#inConditionalWithinScope: boolean = false;
#scopes: Scopes = [];
enter(scope: ReactiveScope, fn: () => void): Set<ReactiveScopeDependency> {
// Save context of previous scope
const prevInConditional = this.#inConditionalWithinScope;
const previousDependencies = this.#dependencies;
const scopedDependencies = new Set<ReactiveScopeDependency>();
// Set context for new scope
// A nested scope should add all deps it directly uses as its own
// unconditional deps, regardless of whether the nested scope is itself
// within a conditional
const scopedDependencies = new Set<ReactiveScopeDependencyInfo>();
this.#inConditionalWithinScope = false;
this.#dependencies = scopedDependencies;
this.#scopes.push(scope);
fn();
// Restore context of previous scope
this.#scopes.pop();
this.#dependencies = previousDependencies;
return deriveMinimalDependencies(scopedDependencies);
this.#inConditionalWithinScope = prevInConditional;
const minScopeDependencies = deriveMinimalDependencies(scopedDependencies);
// propagate dependencies upward using the same rules as normal dependency
// collection. child scopes may have dependencies on values created within
// the outer scope, which necessarily cannot be dependencies of the outer
// scope
for (const dep of minScopeDependencies) {
this.visitDependency({ ...dep, cond: this.#inConditionalWithinScope });
}
return minScopeDependencies;
}
enterConditional(fn: () => void): void {
const prevInConditional = this.#inConditionalWithinScope;
this.#inConditionalWithinScope = true;
fn();
this.#inConditionalWithinScope = prevInConditional;
}
/**
@@ -292,16 +402,18 @@ class Context {
declareProperty(lvalue: Place, object: Place, property: string): void {
const resolvedObject = this.#temporaries.get(object.identifier) ?? object;
const objectDependency = this.#properties.get(resolvedObject.identifier);
let nextDependency: ReactiveScopeDependency;
let nextDependency: ReactiveScopeDependencyInfo;
if (objectDependency === undefined) {
nextDependency = {
identifier: resolvedObject.identifier,
path: [property],
cond: this.#inConditionalWithinScope,
};
} else {
nextDependency = {
identifier: objectDependency.identifier,
path: [...(objectDependency.path ?? []), property],
cond: this.#inConditionalWithinScope,
};
}
this.#properties.set(lvalue.identifier, nextDependency);
@@ -317,29 +429,35 @@ class Context {
visitOperand(place: Place): void {
const resolved = this.#temporaries.get(place.identifier) ?? place;
this.visitDependency({ identifier: resolved.identifier, path: null });
this.visitDependency({
identifier: resolved.identifier,
path: null,
cond: this.#inConditionalWithinScope,
});
}
visitProperty(object: Place, property: string): void {
const resolvedObject = this.#temporaries.get(object.identifier) ?? object;
const objectDependency = this.#properties.get(resolvedObject.identifier);
let nextDependency: ReactiveScopeDependency;
let nextDependency: ReactiveScopeDependencyInfo;
if (objectDependency === undefined) {
nextDependency = {
identifier: resolvedObject.identifier,
path: [property],
cond: this.#inConditionalWithinScope,
};
} else {
nextDependency = {
identifier: objectDependency.identifier,
path: [...(objectDependency.path ?? []), property],
cond: this.#inConditionalWithinScope,
};
}
this.visitDependency(nextDependency);
}
visitDependency(dependency: ReactiveScopeDependency): void {
let maybeDependency: ReactiveScopeDependency;
visitDependency(dependency: ReactiveScopeDependencyInfo): void {
let maybeDependency: ReactiveScopeDependencyInfo;
if (dependency.path !== null) {
// Operands may have memberPaths when propagating depenencies of an inner scope upward
// In this case we use the dependency as-is
@@ -349,12 +467,11 @@ class Context {
// the expanded Place. Fall back to using the operand as-is.
let propDep = this.#properties.get(dependency.identifier);
if (dependency.identifier.name === null && propDep !== undefined) {
maybeDependency = propDep;
maybeDependency = { ...propDep, cond: dependency.cond };
} else {
maybeDependency = dependency;
}
}
// Any value used after its originally defining scope has concluded must be added as an
// output of its defining scope. Regardless of whether its a const or not,
// some later code needs access to the value. If the current
@@ -429,13 +546,6 @@ function visit(context: Context, block: ReactiveBlock): void {
visit(context, item.instructions);
});
item.scope.dependencies = scopeDependencies;
for (const dep of scopeDependencies) {
// propagate dependencies upward using the same rules as
// normal dependency collection. child scopes may have dependencies
// on values created within the outer scope, which necessarily cannot
// be dependencies of the outer scope
context.visitDependency(dep);
}
break;
}
case "instruction": {
@@ -462,30 +572,53 @@ function visit(context: Context, block: ReactiveBlock): void {
case "for": {
visitReactiveValue(context, terminal.init);
visitReactiveValue(context, terminal.test);
visitReactiveValue(context, terminal.update);
visit(context, terminal.loop);
context.enterConditional(() => {
visitReactiveValue(context, terminal.update);
visit(context, terminal.loop);
});
break;
}
case "while": {
visitReactiveValue(context, terminal.test);
visit(context, terminal.loop);
context.enterConditional(() => {
visit(context, terminal.loop);
});
break;
}
case "if": {
context.visitOperand(terminal.test);
visit(context, terminal.consequent);
if (terminal.alternate !== null) {
visit(context, terminal.alternate);
}
/**
* TODO: Track dependencies always accessed within consequent and ones
* always accessed within alternate. If a dependency is always accessed in
* both, we can promote it to an unconditional dependency.
*
* e.g. props.a.b is unconditionally accessed here.
* if (foo(...)) {
* access(props.a.b);
* } else {
* access(props.a.b);
* }
*
* To deal with nested if-branches, enterConditional should return a list
* of dependencies unconditionally accessed within the callback.
*/
context.enterConditional(() => {
visit(context, terminal.consequent);
if (terminal.alternate !== null) {
visit(context, terminal.alternate);
}
});
break;
}
case "switch": {
context.visitOperand(terminal.test);
for (const case_ of terminal.cases) {
if (case_.block !== undefined) {
visit(context, case_.block);
context.enterConditional(() => {
for (const case_ of terminal.cases) {
if (case_.block !== undefined) {
visit(context, case_.block);
}
}
}
});
break;
}
default: {
@@ -508,13 +641,19 @@ function visitReactiveValue(context: Context, value: ReactiveValue): void {
switch (value.kind) {
case "LogicalExpression": {
visitReactiveValue(context, value.left);
visitReactiveValue(context, value.right);
context.enterConditional(() => {
visitReactiveValue(context, value.right);
});
break;
}
case "ConditionalExpression": {
visitReactiveValue(context, value.test);
visitReactiveValue(context, value.consequent);
visitReactiveValue(context, value.alternate);
context.enterConditional(() => {
visitReactiveValue(context, value.consequent);
visitReactiveValue(context, value.alternate);
});
break;
}
case "SequenceExpression": {
@@ -553,9 +692,7 @@ function visitInstructionValue(
context.visitProperty(value.object, value.property);
}
} else {
for (const operand of eachReactiveValueOperand(value)) {
context.visitOperand(operand);
}
visitReactiveValue(context, value);
}
}
@@ -92,13 +92,10 @@ function ComponentA(props) {
* props.b *does* influence `a`
*/
function ComponentB(props) {
const $ = React.unstable_useMemoCache(5);
const c_0 = $[0] !== props.a;
const c_1 = $[1] !== props.b;
const c_2 = $[2] !== props.c;
const c_3 = $[3] !== props.d;
const $ = React.unstable_useMemoCache(2);
const c_0 = $[0] !== props;
let a;
if (c_0 || c_1 || c_2 || c_3) {
if (c_0) {
a = [];
a.push(props.a);
if (props.b) {
@@ -106,13 +103,10 @@ function ComponentB(props) {
}
a.push(props.d);
$[0] = props.a;
$[1] = props.b;
$[2] = props.c;
$[3] = props.d;
$[4] = a;
$[0] = props;
$[1] = a;
} else {
a = $[4];
a = $[1];
}
return a;
}
@@ -121,13 +115,10 @@ function ComponentB(props) {
* props.b *does* influence `a`, but only in a way that is never observable
*/
function ComponentC(props) {
const $ = React.unstable_useMemoCache(5);
const c_0 = $[0] !== props.a;
const c_1 = $[1] !== props.b;
const c_2 = $[2] !== props.c;
const c_3 = $[3] !== props.d;
const $ = React.unstable_useMemoCache(2);
const c_0 = $[0] !== props;
let a;
if (c_0 || c_1 || c_2 || c_3) {
if (c_0) {
a = [];
a.push(props.a);
if (props.b) {
@@ -136,13 +127,10 @@ function ComponentC(props) {
}
a.push(props.d);
$[0] = props.a;
$[1] = props.b;
$[2] = props.c;
$[3] = props.d;
$[4] = a;
$[0] = props;
$[1] = a;
} else {
a = $[4];
a = $[1];
}
return a;
}
@@ -151,13 +139,10 @@ function ComponentC(props) {
* props.b *does* influence `a`
*/
function ComponentD(props) {
const $ = React.unstable_useMemoCache(5);
const c_0 = $[0] !== props.a;
const c_1 = $[1] !== props.b;
const c_2 = $[2] !== props.c;
const c_3 = $[3] !== props.d;
const $ = React.unstable_useMemoCache(2);
const c_0 = $[0] !== props;
let a;
if (c_0 || c_1 || c_2 || c_3) {
if (c_0) {
a = [];
a.push(props.a);
if (props.b) {
@@ -166,13 +151,10 @@ function ComponentD(props) {
}
a.push(props.d);
$[0] = props.a;
$[1] = props.b;
$[2] = props.c;
$[3] = props.d;
$[4] = a;
$[0] = props;
$[1] = a;
} else {
a = $[4];
a = $[1];
}
return a;
}
@@ -35,13 +35,11 @@ function mayMutate() {}
```javascript
function ComponentA(props) {
const $ = React.unstable_useMemoCache(8);
const c_0 = $[0] !== props.p0;
const c_1 = $[1] !== props.p1;
const c_2 = $[2] !== props.p2;
const $ = React.unstable_useMemoCache(6);
const c_0 = $[0] !== props;
let a;
let b;
if (c_0 || c_1 || c_2) {
if (c_0) {
a = [];
b = [];
if (b) {
@@ -50,37 +48,33 @@ function ComponentA(props) {
if (props.p1) {
b.push(props.p2);
}
$[0] = props.p0;
$[1] = props.p1;
$[2] = props.p2;
$[0] = props;
$[1] = a;
$[2] = b;
} else {
a = $[1];
b = $[2];
}
const c_3 = $[3] !== a;
const c_4 = $[4] !== b;
let t0;
if (c_3 || c_4) {
t0 = <Foo a={a} b={b}></Foo>;
$[3] = a;
$[4] = b;
$[5] = t0;
} else {
a = $[3];
b = $[4];
}
const c_5 = $[5] !== a;
const c_6 = $[6] !== b;
let t0;
if (c_5 || c_6) {
t0 = <Foo a={a} b={b}></Foo>;
$[5] = a;
$[6] = b;
$[7] = t0;
} else {
t0 = $[7];
t0 = $[5];
}
return t0;
}
function ComponentB(props) {
const $ = React.unstable_useMemoCache(8);
const c_0 = $[0] !== props.p0;
const c_1 = $[1] !== props.p1;
const c_2 = $[2] !== props.p2;
const $ = React.unstable_useMemoCache(6);
const c_0 = $[0] !== props;
let a;
let b;
if (c_0 || c_1 || c_2) {
if (c_0) {
a = [];
b = [];
if (mayMutate(b)) {
@@ -89,25 +83,23 @@ function ComponentB(props) {
if (props.p1) {
b.push(props.p2);
}
$[0] = props.p0;
$[1] = props.p1;
$[2] = props.p2;
$[0] = props;
$[1] = a;
$[2] = b;
} else {
a = $[1];
b = $[2];
}
const c_3 = $[3] !== a;
const c_4 = $[4] !== b;
let t0;
if (c_3 || c_4) {
t0 = <Foo a={a} b={b}></Foo>;
$[3] = a;
$[4] = b;
$[5] = t0;
} else {
a = $[3];
b = $[4];
}
const c_5 = $[5] !== a;
const c_6 = $[6] !== b;
let t0;
if (c_5 || c_6) {
t0 = <Foo a={a} b={b}></Foo>;
$[5] = a;
$[6] = b;
$[7] = t0;
} else {
t0 = $[7];
t0 = $[5];
}
return t0;
}
@@ -0,0 +1,53 @@
## Input
```javascript
// When an object's properties are only read conditionally, we should
// track the base object as a dependency.
function TestOnlyConditionalDependencies(props, other) {
const x = {};
if (foo(other)) {
x.b = props.a.b;
x.c = props.a.b.c;
}
return x;
}
```
## Code
```javascript
// When an object's properties are only read conditionally, we should
// track the base object as a dependency.
function TestOnlyConditionalDependencies(props, other) {
const $ = React.unstable_useMemoCache(5);
const c_0 = $[0] !== other;
const c_1 = $[1] !== props;
let x;
if (c_0 || c_1) {
x = {};
const c_3 = $[3] !== other;
let t0;
if (c_3) {
t0 = foo(other);
$[3] = other;
$[4] = t0;
} else {
t0 = $[4];
}
if (t0) {
x.b = props.a.b;
x.c = props.a.b.c;
}
$[0] = other;
$[1] = props;
$[2] = x;
} else {
x = $[2];
}
return x;
}
```
@@ -0,0 +1,10 @@
// When an object's properties are only read conditionally, we should
// track the base object as a dependency.
function TestOnlyConditionalDependencies(props, other) {
const x = {};
if (foo(other)) {
x.b = props.a.b;
x.c = props.a.b.c;
}
return x;
}
@@ -0,0 +1,55 @@
## Input
```javascript
// When a conditional dependency `props.a.b.c` has no unconditional dependency
// in its subpath or superpath, we should find the nearest unconditional access
// and promote it to an unconditional dependency.
function TestPromoteUnconditionalAccessToDependency(props, other) {
const x = {};
x.a = props.a.a.a;
if (foo(other)) {
x.c = props.a.b.c;
}
return x;
}
```
## Code
```javascript
// When a conditional dependency `props.a.b.c` has no unconditional dependency
// in its subpath or superpath, we should find the nearest unconditional access
// and promote it to an unconditional dependency.
function TestPromoteUnconditionalAccessToDependency(props, other) {
const $ = React.unstable_useMemoCache(5);
const c_0 = $[0] !== props.a;
const c_1 = $[1] !== other;
let x;
if (c_0 || c_1) {
x = {};
x.a = props.a.a.a;
const c_3 = $[3] !== other;
let t0;
if (c_3) {
t0 = foo(other);
$[3] = other;
$[4] = t0;
} else {
t0 = $[4];
}
if (t0) {
x.c = props.a.b.c;
}
$[0] = props.a;
$[1] = other;
$[2] = x;
} else {
x = $[2];
}
return x;
}
```
@@ -0,0 +1,11 @@
// When a conditional dependency `props.a.b.c` has no unconditional dependency
// in its subpath or superpath, we should find the nearest unconditional access
// and promote it to an unconditional dependency.
function TestPromoteUnconditionalAccessToDependency(props, other) {
const x = {};
x.a = props.a.a.a;
if (foo(other)) {
x.c = props.a.b.c;
}
return x;
}
@@ -0,0 +1,59 @@
## Input
```javascript
// When a conditional dependency `props.a` is a subpath of an unconditional
// dependency `props.a.b`, we can access `props.a` while preserving program
// semantics (with respect to nullthrows).
// deps: {`props.a`, `props.a.b`} can further reduce to just `props.a`
// ordering of accesses should not matter
function TestConditionalSubpath1(props, other) {
const x = {};
x.b = props.a.b;
if (foo(other)) {
x.a = props.a;
}
return x;
}
```
## Code
```javascript
// When a conditional dependency `props.a` is a subpath of an unconditional
// dependency `props.a.b`, we can access `props.a` while preserving program
// semantics (with respect to nullthrows).
// deps: {`props.a`, `props.a.b`} can further reduce to just `props.a`
// ordering of accesses should not matter
function TestConditionalSubpath1(props, other) {
const $ = React.unstable_useMemoCache(5);
const c_0 = $[0] !== props.a;
const c_1 = $[1] !== other;
let x;
if (c_0 || c_1) {
x = {};
x.b = props.a.b;
const c_3 = $[3] !== other;
let t0;
if (c_3) {
t0 = foo(other);
$[3] = other;
$[4] = t0;
} else {
t0 = $[4];
}
if (t0) {
x.a = props.a;
}
$[0] = props.a;
$[1] = other;
$[2] = x;
} else {
x = $[2];
}
return x;
}
```
@@ -0,0 +1,13 @@
// When a conditional dependency `props.a` is a subpath of an unconditional
// dependency `props.a.b`, we can access `props.a` while preserving program
// semantics (with respect to nullthrows).
// deps: {`props.a`, `props.a.b`} can further reduce to just `props.a`
// ordering of accesses should not matter
function TestConditionalSubpath1(props, other) {
const x = {};
x.b = props.a.b;
if (foo(other)) {
x.a = props.a;
}
return x;
}
@@ -0,0 +1,59 @@
## Input
```javascript
// When a conditional dependency `props.a` is a subpath of an unconditional
// dependency `props.a.b`, we can access `props.a` while preserving program
// semantics (with respect to nullthrows).
// deps: {`props.a`, `props.a.b`} can further reduce to just `props.a`
// ordering of accesses should not matter
function TestConditionalSubpath2(props, other) {
const x = {};
if (foo(other)) {
x.a = props.a;
}
x.b = props.a.b;
return x;
}
```
## Code
```javascript
// When a conditional dependency `props.a` is a subpath of an unconditional
// dependency `props.a.b`, we can access `props.a` while preserving program
// semantics (with respect to nullthrows).
// deps: {`props.a`, `props.a.b`} can further reduce to just `props.a`
// ordering of accesses should not matter
function TestConditionalSubpath2(props, other) {
const $ = React.unstable_useMemoCache(5);
const c_0 = $[0] !== other;
const c_1 = $[1] !== props.a;
let x;
if (c_0 || c_1) {
x = {};
const c_3 = $[3] !== other;
let t0;
if (c_3) {
t0 = foo(other);
$[3] = other;
$[4] = t0;
} else {
t0 = $[4];
}
if (t0) {
x.a = props.a;
}
x.b = props.a.b;
$[0] = other;
$[1] = props.a;
$[2] = x;
} else {
x = $[2];
}
return x;
}
```
@@ -0,0 +1,13 @@
// When a conditional dependency `props.a` is a subpath of an unconditional
// dependency `props.a.b`, we can access `props.a` while preserving program
// semantics (with respect to nullthrows).
// deps: {`props.a`, `props.a.b`} can further reduce to just `props.a`
// ordering of accesses should not matter
function TestConditionalSubpath2(props, other) {
const x = {};
if (foo(other)) {
x.a = props.a;
}
x.b = props.a.b;
return x;
}
@@ -0,0 +1,57 @@
## Input
```javascript
// When an unconditional dependency `props.a` is the subpath of a conditional
// dependency `props.a.b`, we can safely overestimate and only track `props.a`
// as a dependency
// ordering of accesses should not matter
function TestConditionalSuperpath1(props, other) {
const x = {};
x.a = props.a;
if (foo(other)) {
x.b = props.a.b;
}
return x;
}
```
## Code
```javascript
// When an unconditional dependency `props.a` is the subpath of a conditional
// dependency `props.a.b`, we can safely overestimate and only track `props.a`
// as a dependency
// ordering of accesses should not matter
function TestConditionalSuperpath1(props, other) {
const $ = React.unstable_useMemoCache(5);
const c_0 = $[0] !== props.a;
const c_1 = $[1] !== other;
let x;
if (c_0 || c_1) {
x = {};
x.a = props.a;
const c_3 = $[3] !== other;
let t0;
if (c_3) {
t0 = foo(other);
$[3] = other;
$[4] = t0;
} else {
t0 = $[4];
}
if (t0) {
x.b = props.a.b;
}
$[0] = props.a;
$[1] = other;
$[2] = x;
} else {
x = $[2];
}
return x;
}
```
@@ -0,0 +1,12 @@
// When an unconditional dependency `props.a` is the subpath of a conditional
// dependency `props.a.b`, we can safely overestimate and only track `props.a`
// as a dependency
// ordering of accesses should not matter
function TestConditionalSuperpath1(props, other) {
const x = {};
x.a = props.a;
if (foo(other)) {
x.b = props.a.b;
}
return x;
}
@@ -0,0 +1,57 @@
## Input
```javascript
// When an unconditional dependency `props.a` is the subpath of a conditional
// dependency `props.a.b`, we can safely overestimate and only track `props.a`
// as a dependency
// ordering of accesses should not matter
function TestConditionalSuperpath2(props, other) {
const x = {};
if (foo(other)) {
x.b = props.a.b;
}
x.a = props.a;
return x;
}
```
## Code
```javascript
// When an unconditional dependency `props.a` is the subpath of a conditional
// dependency `props.a.b`, we can safely overestimate and only track `props.a`
// as a dependency
// ordering of accesses should not matter
function TestConditionalSuperpath2(props, other) {
const $ = React.unstable_useMemoCache(5);
const c_0 = $[0] !== other;
const c_1 = $[1] !== props.a;
let x;
if (c_0 || c_1) {
x = {};
const c_3 = $[3] !== other;
let t0;
if (c_3) {
t0 = foo(other);
$[3] = other;
$[4] = t0;
} else {
t0 = $[4];
}
if (t0) {
x.b = props.a.b;
}
x.a = props.a;
$[0] = other;
$[1] = props.a;
$[2] = x;
} else {
x = $[2];
}
return x;
}
```
@@ -0,0 +1,12 @@
// When an unconditional dependency `props.a` is the subpath of a conditional
// dependency `props.a.b`, we can safely overestimate and only track `props.a`
// as a dependency
// ordering of accesses should not matter
function TestConditionalSuperpath2(props, other) {
const x = {};
if (foo(other)) {
x.b = props.a.b;
}
x.a = props.a;
return x;
}
@@ -0,0 +1,82 @@
## Input
```javascript
// Some reactive scopes are created within a conditional. If a child scope
// is within a conditional, its reactive dependencies should be propagated
// as conditionals
//
// In this test:
// ```javascript
// scope @0 (deps=[???] decls=[x]) {
// const x = {};
// if (foo) {
// scope @1 (deps=[props.a.b] decls=[tmp]) {
// const tmp = bar(props.a.b);
// }
// x.a = tmp;
// }
// }
// return x;
// ```
function TestReactiveDepsInCondScope(props) {
let x = {};
if (foo) {
let tmp = bar(props.a.b);
x.a = tmp;
}
return x;
}
```
## Code
```javascript
// Some reactive scopes are created within a conditional. If a child scope
// is within a conditional, its reactive dependencies should be propagated
// as conditionals
//
// In this test:
// ```javascript
// scope @0 (deps=[???] decls=[x]) {
// const x = {};
// if (foo) {
// scope @1 (deps=[props.a.b] decls=[tmp]) {
// const tmp = bar(props.a.b);
// }
// x.a = tmp;
// }
// }
// return x;
// ```
function TestReactiveDepsInCondScope(props) {
const $ = React.unstable_useMemoCache(4);
const c_0 = $[0] !== props;
let x;
if (c_0) {
x = {};
if (foo) {
const c_2 = $[2] !== props.a.b;
let tmp;
if (c_2) {
tmp = bar(props.a.b);
$[2] = props.a.b;
$[3] = tmp;
} else {
tmp = $[3];
}
x.a = tmp;
}
$[0] = props;
$[1] = x;
} else {
x = $[1];
}
return x;
}
```
@@ -0,0 +1,26 @@
// Some reactive scopes are created within a conditional. If a child scope
// is within a conditional, its reactive dependencies should be propagated
// as conditionals
//
// In this test:
// ```javascript
// scope @0 (deps=[???] decls=[x]) {
// const x = {};
// if (foo) {
// scope @1 (deps=[props.a.b] decls=[tmp]) {
// const tmp = bar(props.a.b);
// }
// x.a = tmp;
// }
// }
// return x;
// ```
function TestReactiveDepsInCondScope(props) {
let x = {};
if (foo) {
let tmp = bar(props.a.b);
x.a = tmp;
}
return x;
}
@@ -57,12 +57,12 @@ function foo(props) {
} else {
x = $[1];
}
const c_2 = $[2] !== props;
const c_2 = $[2] !== props.showHeader;
const c_3 = $[3] !== x;
let t0;
if (c_2 || c_3) {
t0 = props.showHeader ? <div>{x}</div> : null;
$[2] = props;
$[2] = props.showHeader;
$[3] = x;
$[4] = t0;
} else {
@@ -23,41 +23,39 @@ function Component(props) {
```javascript
function Component(props) {
const $ = React.unstable_useMemoCache(7);
const c_0 = $[0] !== props.p0;
const c_1 = $[1] !== props.p1;
const $ = React.unstable_useMemoCache(6);
const c_0 = $[0] !== props;
let x;
let y;
if (c_0 || c_1) {
if (c_0) {
x = [];
y = undefined;
if (props.p0) {
x.push(props.p1);
y = x;
}
$[0] = props.p0;
$[1] = props.p1;
$[2] = x;
$[3] = y;
$[0] = props;
$[1] = x;
$[2] = y;
} else {
x = $[2];
y = $[3];
x = $[1];
y = $[2];
}
const c_4 = $[4] !== x;
const c_5 = $[5] !== y;
const c_3 = $[3] !== x;
const c_4 = $[4] !== y;
let t0;
if (c_4 || c_5) {
if (c_3 || c_4) {
t0 = (
<Component>
{x}
{y}
</Component>
);
$[4] = x;
$[5] = y;
$[6] = t0;
$[3] = x;
$[4] = y;
$[5] = t0;
} else {
t0 = $[6];
t0 = $[5];
}
return t0;
}
@@ -20,12 +20,10 @@ function foo(props) {
```javascript
function foo(props) {
const $ = React.unstable_useMemoCache(4);
const c_0 = $[0] !== props.bar;
const c_1 = $[1] !== props.cond;
const c_2 = $[2] !== props.foo;
const $ = React.unstable_useMemoCache(2);
const c_0 = $[0] !== props;
let x;
if (c_0 || c_1 || c_2) {
if (c_0) {
x = [];
x.push(props.bar);
if (props.cond) {
@@ -34,12 +32,10 @@ function foo(props) {
}
mut(x);
$[0] = props.bar;
$[1] = props.cond;
$[2] = props.foo;
$[3] = x;
$[0] = props;
$[1] = x;
} else {
x = $[3];
x = $[1];
}
return x;
}
@@ -32,12 +32,11 @@ function Component(props) {
```javascript
function Component(props) {
const $ = React.unstable_useMemoCache(10);
const c_0 = $[0] !== props.p0;
const c_1 = $[1] !== props.p2;
const $ = React.unstable_useMemoCache(9);
const c_0 = $[0] !== props;
let x;
let y;
if (c_0 || c_1) {
if (c_0) {
x = [];
y = undefined;
bb1: switch (props.p0) {
@@ -47,11 +46,11 @@ function Component(props) {
case true: {
x.push(props.p2);
let t0;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [];
$[4] = t0;
$[3] = t0;
} else {
t0 = $[4];
t0 = $[3];
}
y = t0;
break bb1;
@@ -63,34 +62,33 @@ function Component(props) {
y = x;
}
}
$[0] = props.p0;
$[1] = props.p2;
$[2] = x;
$[3] = y;
$[0] = props;
$[1] = x;
$[2] = y;
} else {
x = $[2];
y = $[3];
x = $[1];
y = $[2];
}
const c_5 = $[5] !== x;
const c_4 = $[4] !== x;
let child;
if (c_5) {
if (c_4) {
child = <Component data={x}></Component>;
$[5] = x;
$[6] = child;
$[4] = x;
$[5] = child;
} else {
child = $[6];
child = $[5];
}
y.push(props.p4);
const c_7 = $[7] !== y;
const c_8 = $[8] !== child;
const c_6 = $[6] !== y;
const c_7 = $[7] !== child;
let t1;
if (c_7 || c_8) {
if (c_6 || c_7) {
t1 = <Component data={y}>{child}</Component>;
$[7] = y;
$[8] = child;
$[9] = t1;
$[6] = y;
$[7] = child;
$[8] = t1;
} else {
t1 = $[9];
t1 = $[8];
}
return t1;
}
@@ -27,13 +27,11 @@ function Component(props) {
```javascript
function Component(props) {
const $ = React.unstable_useMemoCache(10);
const c_0 = $[0] !== props.p0;
const c_1 = $[1] !== props.p2;
const c_2 = $[2] !== props.p3;
const $ = React.unstable_useMemoCache(8);
const c_0 = $[0] !== props;
let x;
let y;
if (c_0 || c_1 || c_2) {
if (c_0) {
x = [];
y = undefined;
switch (props.p0) {
@@ -45,35 +43,33 @@ function Component(props) {
y = x;
}
}
$[0] = props.p0;
$[1] = props.p2;
$[2] = props.p3;
$[3] = x;
$[4] = y;
$[0] = props;
$[1] = x;
$[2] = y;
} else {
x = $[3];
y = $[4];
x = $[1];
y = $[2];
}
const c_5 = $[5] !== x;
const c_3 = $[3] !== x;
let child;
if (c_5) {
if (c_3) {
child = <Component data={x}></Component>;
$[5] = x;
$[6] = child;
$[3] = x;
$[4] = child;
} else {
child = $[6];
child = $[4];
}
y.push(props.p4);
const c_7 = $[7] !== y;
const c_8 = $[8] !== child;
const c_5 = $[5] !== y;
const c_6 = $[6] !== child;
let t0;
if (c_7 || c_8) {
if (c_5 || c_6) {
t0 = <Component data={y}>{child}</Component>;
$[7] = y;
$[8] = child;
$[9] = t0;
$[5] = y;
$[6] = child;
$[7] = t0;
} else {
t0 = $[9];
t0 = $[7];
}
return t0;
}