Memoize arrays/objects created with destructuring spread

We were treating Destructuring as if it could never allocate and therefore 
didn't have to be memoized. That's only true if there are no rest spreads 
though. This PR teaches the compiler to treat rest spreads differently for 
scoping and memoization 

purposes, fixing the newly added test case and some existing bugs.
This commit is contained in:
Joe Savona
2023-03-09 12:44:30 -08:00
parent fcbfca69c9
commit d376cf1e37
6 changed files with 235 additions and 77 deletions
+28
View File
@@ -185,6 +185,34 @@ export function* eachInstructionValueOperand(
}
}
export function doesPatternContainSpreadElement(pattern: Pattern): boolean {
switch (pattern.kind) {
case "ArrayPattern": {
for (const item of pattern.items) {
if (item.kind === "Spread") {
return true;
}
}
break;
}
case "ObjectPattern": {
for (const property of pattern.properties) {
if (property.kind === "Spread") {
return true;
}
}
break;
}
default: {
assertExhaustive(
pattern,
`Unexpected pattern kind '${(pattern as any).kind}'`
);
}
}
return false;
}
export function* eachPatternOperand(pattern: Pattern): Iterable<Place> {
switch (pattern.kind) {
case "ArrayPattern": {
@@ -15,7 +15,11 @@ import {
Place,
ReactiveScope,
} from "../HIR/HIR";
import { eachInstructionOperand, eachPatternOperand } from "../HIR/visitors";
import {
doesPatternContainSpreadElement,
eachInstructionOperand,
eachPatternOperand,
} from "../HIR/visitors";
import DisjointSet from "../Utils/DisjointSet";
import { assertExhaustive } from "../Utils/utils";
@@ -198,7 +202,9 @@ function isMutable({ id }: Instruction, place: Place): boolean {
function mayAllocate(value: InstructionValue): boolean {
switch (value.kind) {
case "Destructure":
case "Destructure": {
return doesPatternContainSpreadElement(value.lvalue.pattern);
}
case "StoreLocal":
case "LoadGlobal":
case "TypeCastExpression":
@@ -12,6 +12,7 @@ import {
Effect,
IdentifierId,
InstructionId,
Pattern,
Place,
ReactiveFunction,
ReactiveInstruction,
@@ -22,7 +23,6 @@ import {
ReactiveValue,
ScopeId,
} from "../HIR";
import { eachPatternOperand } from "../HIR/visitors";
import { log } from "../Utils/logger";
import { assertExhaustive } from "../Utils/utils";
import { getPlaceScope } from "./BuildReactiveBlocks";
@@ -319,43 +319,48 @@ function computeMemoizationInputs(
lvalue: Place | null
): {
// can optionally return a custom set of lvalues per instruction
lvalues: Array<Place>;
lvalues: Array<LValueMemoization>;
rvalues: Array<Place>;
level: MemoizationLevel;
} {
switch (value.kind) {
case "ConditionalExpression": {
return {
lvalues: lvalue !== null ? [lvalue] : [],
// Only need to memoize if the rvalues are memoized
lvalues:
lvalue !== null
? [{ place: lvalue, level: MemoizationLevel.Conditional }]
: [],
rvalues: [
// Conditionals do not alias their test value.
...computeMemoizationInputs(value.consequent, null).rvalues,
...computeMemoizationInputs(value.alternate, null).rvalues,
],
// Only need to memoize if the rvalues are memoized
level: MemoizationLevel.Conditional,
};
}
case "LogicalExpression": {
return {
lvalues: lvalue !== null ? [lvalue] : [],
// Only need to memoize if the rvalues are memoized
lvalues:
lvalue !== null
? [{ place: lvalue, level: MemoizationLevel.Conditional }]
: [],
rvalues: [
...computeMemoizationInputs(value.left, null).rvalues,
...computeMemoizationInputs(value.right, null).rvalues,
],
// Only need to memoize if the rvalues are memoized
level: MemoizationLevel.Conditional,
};
}
case "SequenceExpression": {
return {
lvalues: lvalue !== null ? [lvalue] : [],
// Only need to memoize if the rvalues are memoized
lvalues:
lvalue !== null
? [{ place: lvalue, level: MemoizationLevel.Conditional }]
: [],
// Only the final value of the sequence is a true rvalue:
// values from the sequence's instructions are evaluated
// as separate nodes
rvalues: computeMemoizationInputs(value.value, null).rvalues,
// Only memoize if the final value was memoized
level: MemoizationLevel.Conditional,
};
}
case "JsxExpression": {
@@ -374,20 +379,24 @@ function computeMemoizationInputs(
}
}
return {
lvalues: lvalue !== null ? [lvalue] : [],
rvalues: operands,
// JSX elements themselves are not memoized unless forced to
// avoid breaking downstream memoization
level: MemoizationLevel.Unmemoized,
lvalues:
lvalue !== null
? [{ place: lvalue, level: MemoizationLevel.Unmemoized }]
: [],
rvalues: operands,
};
}
case "JsxFragment": {
return {
lvalues: lvalue !== null ? [lvalue] : [],
rvalues: value.children,
// JSX elements themselves are not memoized unless forced to
// avoid breaking downstream memoization
level: MemoizationLevel.Unmemoized,
lvalues:
lvalue !== null
? [{ place: lvalue, level: MemoizationLevel.Unmemoized }]
: [],
rvalues: value.children,
};
}
case "ComputedDelete":
@@ -399,68 +408,84 @@ function computeMemoizationInputs(
case "BinaryExpression":
case "UnaryExpression": {
return {
lvalues: lvalue !== null ? [lvalue] : [],
rvalues: [],
// All of these instructions return a primitive value and never need to be memoized
level: MemoizationLevel.Never,
lvalues:
lvalue !== null
? [{ place: lvalue, level: MemoizationLevel.Never }]
: [],
rvalues: [],
};
}
case "TypeCastExpression": {
return {
lvalues: lvalue !== null ? [lvalue] : [],
// Indirection for the inner value, memoized if the value is
lvalues:
lvalue !== null
? [{ place: lvalue, level: MemoizationLevel.Conditional }]
: [],
rvalues: [value.value],
level: MemoizationLevel.Conditional,
};
}
case "LoadLocal": {
return {
lvalues: lvalue !== null ? [lvalue] : [],
// Indirection for the inner value, memoized if the value is
lvalues:
lvalue !== null
? [{ place: lvalue, level: MemoizationLevel.Conditional }]
: [],
rvalues: [value.place],
level: MemoizationLevel.Conditional,
};
}
case "StoreLocal": {
const lvalues = [
{ place: value.lvalue.place, level: MemoizationLevel.Conditional },
];
if (lvalue !== null) {
lvalues.push({ place: lvalue, level: MemoizationLevel.Conditional });
}
return {
lvalues:
lvalue !== null ? [lvalue, value.lvalue.place] : [value.lvalue.place],
// Indirection for the inner value, memoized if the value is
lvalues,
rvalues: [value.value],
level: MemoizationLevel.Conditional,
};
}
case "Destructure": {
// Indirection for the inner value, memoized if the value is
const lvalues = [];
if (lvalue !== null) {
lvalues.push(lvalue);
lvalues.push({ place: lvalue, level: MemoizationLevel.Conditional });
}
lvalues.push(...eachPatternOperand(value.lvalue.pattern));
lvalues.push(...computePatternLValues(value.lvalue.pattern));
return {
lvalues: lvalues,
// Indirection for the inner value, memoized if the value is
rvalues: [value.value],
level: MemoizationLevel.Conditional,
};
}
case "ComputedLoad":
case "PropertyLoad": {
return {
lvalues: lvalue !== null ? [lvalue] : [],
// Indirection for the inner value, memoized if the value is
lvalues:
lvalue !== null
? [{ place: lvalue, level: MemoizationLevel.Conditional }]
: [],
// Only the object is aliased to the result, and the result only needs to be
// memoized if the object is
rvalues: [value.object],
level: MemoizationLevel.Conditional,
};
}
case "ComputedStore": {
// The object being stored to acts as an lvalue (it aliases the value), but
// the computed key is not aliased
const lvalues = [
{ place: value.object, level: MemoizationLevel.Conditional },
];
if (lvalue !== null) {
lvalues.push({ place: lvalue, level: MemoizationLevel.Conditional });
}
return {
lvalues: lvalue !== null ? [lvalue, value.object] : [value.object],
lvalues,
rvalues: [value.value],
level: MemoizationLevel.Conditional,
};
}
case "FunctionExpression":
@@ -475,16 +500,15 @@ function computeMemoizationInputs(
// All of these instructions may produce new values which must be memoized if
// reachable from a return value. Any mutable rvalue may alias any other rvalue
const operands = [...eachReactiveValueOperand(value)];
const lvalues = operands.filter((operand) =>
isMutableEffect(operand.effect)
);
const lvalues = operands
.filter((operand) => isMutableEffect(operand.effect))
.map((place) => ({ place, level: MemoizationLevel.Memoized }));
if (lvalue !== null) {
lvalues.push(lvalue);
lvalues.push({ place: lvalue, level: MemoizationLevel.Memoized });
}
return {
lvalues,
rvalues: operands,
level: MemoizationLevel.Memoized,
};
}
case "UnsupportedNode": {
@@ -496,6 +520,45 @@ function computeMemoizationInputs(
}
}
function computePatternLValues(pattern: Pattern): Array<LValueMemoization> {
const lvalues: Array<LValueMemoization> = [];
switch (pattern.kind) {
case "ArrayPattern": {
for (const item of pattern.items) {
if (item.kind === "Identifier") {
lvalues.push({ place: item, level: MemoizationLevel.Conditional });
} else {
lvalues.push({ place: item.place, level: MemoizationLevel.Memoized });
}
}
break;
}
case "ObjectPattern": {
for (const property of pattern.properties) {
if (property.kind === "ObjectProperty") {
lvalues.push({
place: property.place,
level: MemoizationLevel.Conditional,
});
} else {
lvalues.push({
place: property.place,
level: MemoizationLevel.Memoized,
});
}
}
break;
}
default: {
assertExhaustive(
pattern,
`Unexpected pattern kind '${(pattern as any).kind}'`
);
}
}
return lvalues;
}
/**
* Populates the input state with the set of returned identifiers and information about each
* identifier's and scope's dependencies.
@@ -521,7 +584,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
}
// Add the operands as dependencies of all lvalues.
for (const lvalue of aliasing.lvalues) {
for (const { place: lvalue, level } of aliasing.lvalues) {
const lvalueId =
state.definitions.get(lvalue.identifier.id) ?? lvalue.identifier.id;
let node = state.identifiers.get(lvalueId);
@@ -535,7 +598,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
};
state.identifiers.set(lvalueId, node);
}
node.level = joinAliases(node.level, aliasing.level);
node.level = joinAliases(node.level, level);
// This looks like NxM iterations but in practice all instructions with multiple
// lvalues have only a single rvalue
for (const operand of aliasing.rvalues) {
@@ -28,39 +28,75 @@ function foo(a, b, c) {
```javascript
function foo(a, b, c) {
const $ = React.unstable_useMemoCache(8);
const [d, t40, ...h] = a;
const $ = React.unstable_useMemoCache(18);
const c_0 = $[0] !== a;
let t0;
let d;
let h;
if (c_0) {
[d, t0, ...h] = a;
$[0] = a;
$[1] = t0;
$[2] = d;
$[3] = h;
} else {
t0 = $[1];
d = $[2];
h = $[3];
}
const [t43] = t40;
const { e: t45, ...g } = t43;
const { f } = t45;
const [t1] = t0;
const c_4 = $[4] !== t1;
let t2;
let g;
if (c_4) {
({ e: t2, ...g } = t1);
$[4] = t1;
$[5] = t2;
$[6] = g;
} else {
t2 = $[5];
g = $[6];
}
const { f } = t2;
const { l: t51, p } = b;
const { m: t54 } = t51;
const [t56, ...o] = t54;
const [n] = t56;
const c_0 = $[0] !== d;
const c_1 = $[1] !== f;
const c_2 = $[2] !== g;
const c_3 = $[3] !== h;
const c_4 = $[4] !== n;
const c_5 = $[5] !== o;
const c_6 = $[6] !== p;
let t0;
if (c_0 || c_1 || c_2 || c_3 || c_4 || c_5 || c_6) {
t0 = [d, f, g, h, n, o, p];
$[0] = d;
$[1] = f;
$[2] = g;
$[3] = h;
$[4] = n;
$[5] = o;
$[6] = p;
$[7] = t0;
const { m: t3 } = t51;
const c_7 = $[7] !== t3;
let t4;
let o;
if (c_7) {
[t4, ...o] = t3;
$[7] = t3;
$[8] = t4;
$[9] = o;
} else {
t0 = $[7];
t4 = $[8];
o = $[9];
}
return t0;
const [n] = t4;
const c_10 = $[10] !== d;
const c_11 = $[11] !== f;
const c_12 = $[12] !== g;
const c_13 = $[13] !== h;
const c_14 = $[14] !== n;
const c_15 = $[15] !== o;
const c_16 = $[16] !== p;
let t5;
if (c_10 || c_11 || c_12 || c_13 || c_14 || c_15 || c_16) {
t5 = [d, f, g, h, n, o, p];
$[10] = d;
$[11] = f;
$[12] = g;
$[13] = h;
$[14] = n;
$[15] = o;
$[16] = p;
$[17] = t5;
} else {
t5 = $[17];
}
return t5;
}
```
@@ -16,9 +16,25 @@ function Component(props) {
```javascript
function Component(props) {
const { a, ...b } = props.a;
const [c, ...d] = props.c;
const $ = React.unstable_useMemoCache(4);
const c_0 = $[0] !== props.a;
let b;
if (c_0) {
({ a, ...b } = props.a);
$[0] = props.a;
$[1] = b;
} else {
b = $[1];
}
const c_2 = $[2] !== props.c;
let d;
if (c_2) {
[c, ...d] = props.c;
$[2] = props.c;
$[3] = d;
} else {
d = $[3];
}
return <div b={b} d={d}></div>;
}
@@ -14,7 +14,16 @@ function Foo(props) {
```javascript
function Foo(props) {
const { unused, ...rest } = props.a;
const $ = React.unstable_useMemoCache(2);
const c_0 = $[0] !== props.a;
let rest;
if (c_0) {
({ unused, ...rest } = props.a);
$[0] = props.a;
$[1] = rest;
} else {
rest = $[1];
}
return rest;
}