diff --git a/compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts b/compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts index 55e439decd..01f61a88fe 100644 --- a/compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts +++ b/compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts @@ -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; // 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 { const results: Array = []; 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 + initialDeps: Set ): Set { const depRoots = new Map(); @@ -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 = new Map(); - #dependencies: Set = new Set(); - #properties: Map = new Map(); + #dependencies: Set = new Set(); + #properties: Map = new Map(); #temporaries: Map = new Map(); + #inConditionalWithinScope: boolean = false; #scopes: Scopes = []; enter(scope: ReactiveScope, fn: () => void): Set { + // Save context of previous scope + const prevInConditional = this.#inConditionalWithinScope; const previousDependencies = this.#dependencies; - const scopedDependencies = new Set(); + + // 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(); + 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); } } diff --git a/compiler/forget/src/__tests__/fixtures/hir/conditional-break.expect.md b/compiler/forget/src/__tests__/fixtures/hir/conditional-break.expect.md index 987ca2029c..dd7187221c 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/conditional-break.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/conditional-break.expect.md @@ -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; } diff --git a/compiler/forget/src/__tests__/fixtures/hir/conditional-on-mutable.expect.md b/compiler/forget/src/__tests__/fixtures/hir/conditional-on-mutable.expect.md index 0868b722f4..f80aacf62e 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/conditional-on-mutable.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/conditional-on-mutable.expect.md @@ -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 = ; $[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 = ; - $[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 = ; $[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 = ; - $[5] = a; - $[6] = b; - $[7] = t0; - } else { - t0 = $[7]; + t0 = $[5]; } return t0; } diff --git a/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-no-uncond.expect.md b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-no-uncond.expect.md new file mode 100644 index 0000000000..8b60cacc88 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-no-uncond.expect.md @@ -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; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-no-uncond.js b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-no-uncond.js new file mode 100644 index 0000000000..a4deaf7886 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-no-uncond.js @@ -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; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-promote-uncond.expect.md b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-promote-uncond.expect.md new file mode 100644 index 0000000000..0dc9bc311e --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-promote-uncond.expect.md @@ -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; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-promote-uncond.js b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-promote-uncond.js new file mode 100644 index 0000000000..debbbd34e6 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-promote-uncond.js @@ -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; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-subpath-order1.expect.md b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-subpath-order1.expect.md new file mode 100644 index 0000000000..6b15c1d174 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-subpath-order1.expect.md @@ -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; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-subpath-order1.js b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-subpath-order1.js new file mode 100644 index 0000000000..b0fb33839c --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-subpath-order1.js @@ -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; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-subpath-order2.expect.md b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-subpath-order2.expect.md new file mode 100644 index 0000000000..e7a9a37f80 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-subpath-order2.expect.md @@ -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; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-subpath-order2.js b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-subpath-order2.js new file mode 100644 index 0000000000..dc9d7afed0 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-subpath-order2.js @@ -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; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-superpath-order1.expect.md b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-superpath-order1.expect.md new file mode 100644 index 0000000000..ac67e5dd5e --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-superpath-order1.expect.md @@ -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; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-superpath-order1.js b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-superpath-order1.js new file mode 100644 index 0000000000..67c2e91947 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-superpath-order1.js @@ -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; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-superpath-order2.expect.md b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-superpath-order2.expect.md new file mode 100644 index 0000000000..52fdceb006 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-superpath-order2.expect.md @@ -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; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-superpath-order2.js b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-superpath-order2.js new file mode 100644 index 0000000000..2ea7f3d9a5 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-superpath-order2.js @@ -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; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-deps-cond-scope.expect.md b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-deps-cond-scope.expect.md new file mode 100644 index 0000000000..8cea94f8b8 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-deps-cond-scope.expect.md @@ -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; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-deps-cond-scope.js b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-deps-cond-scope.js new file mode 100644 index 0000000000..0a5e0a16dd --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-deps-cond-scope.js @@ -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; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/same-variable-as-dep-and-redeclare-maybe-frozen.expect.md b/compiler/forget/src/__tests__/fixtures/hir/same-variable-as-dep-and-redeclare-maybe-frozen.expect.md index fe1fc2233d..4ba3d1f238 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/same-variable-as-dep-and-redeclare-maybe-frozen.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/same-variable-as-dep-and-redeclare-maybe-frozen.expect.md @@ -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 ?
{x}
: null; - $[2] = props; + $[2] = props.showHeader; $[3] = x; $[4] = t0; } else { diff --git a/compiler/forget/src/__tests__/fixtures/hir/ssa-leave-case.expect.md b/compiler/forget/src/__tests__/fixtures/hir/ssa-leave-case.expect.md index acbd971729..72c1e52d44 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/ssa-leave-case.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/ssa-leave-case.expect.md @@ -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 = ( {x} {y} ); - $[4] = x; - $[5] = y; - $[6] = t0; + $[3] = x; + $[4] = y; + $[5] = t0; } else { - t0 = $[6]; + t0 = $[5]; } return t0; } diff --git a/compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-with-mutation.expect.md b/compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-with-mutation.expect.md index 561a57e546..b898ae5474 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-with-mutation.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-with-mutation.expect.md @@ -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; } diff --git a/compiler/forget/src/__tests__/fixtures/hir/switch-non-final-default.expect.md b/compiler/forget/src/__tests__/fixtures/hir/switch-non-final-default.expect.md index cad7c3388a..efe6294bf7 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/switch-non-final-default.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/switch-non-final-default.expect.md @@ -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 = ; - $[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 = {child}; - $[7] = y; - $[8] = child; - $[9] = t1; + $[6] = y; + $[7] = child; + $[8] = t1; } else { - t1 = $[9]; + t1 = $[8]; } return t1; } diff --git a/compiler/forget/src/__tests__/fixtures/hir/switch.expect.md b/compiler/forget/src/__tests__/fixtures/hir/switch.expect.md index 5375018884..7f1b6fd659 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/switch.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/switch.expect.md @@ -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 = ; - $[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 = {child}; - $[7] = y; - $[8] = child; - $[9] = t0; + $[5] = y; + $[6] = child; + $[7] = t0; } else { - t0 = $[9]; + t0 = $[7]; } return t0; }