Record reassignments
Record a variable that is declared in some other scope and that is being reassigned in the current one as a reassignment
Lauren Tan committed
Feb 8, 2023 at 10:26 UTC
d47f608c612082b82d9d5c79f696f86e394a4d1a
9 files changed
+159
-72
compiler/forget/src/HIR/HIR.ts
+1
@@ -556,6 +556,7 @@ export type ReactiveScope = {
556
range: MutableRange;
557
dependencies: Set<ReactiveScopeDependency>;
558
declarations: Set<Identifier>;
559
+ reassignments: Set<Identifier>;
560
};
561
562
export type ReactiveScopeDependency = {
compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+43
-16
@@ -76,7 +76,7 @@ export function codegenReactiveFunction(
76
77
class Context {
78
#nextCacheIndex: number = 0;
79
- #identifiers: Set<Identifier> = new Set();
79
+ #declarations: Set<Identifier> = new Set();
80
temp: Temporaries = new Map();
81
errors: CompilerError = new CompilerError();
82
@@ -85,11 +85,11 @@ class Context {
85
}
86
87
declare(identifier: Identifier): void {
88
- this.#identifiers.add(identifier);
88
+ this.#declarations.add(identifier);
89
}
90
91
- declared(identifier: Identifier): boolean {
92
- return this.#identifiers.has(identifier);
91
+ hasDeclared(identifier: Identifier): boolean {
92
+ return this.#declarations.has(identifier);
93
}
94
}
95
@@ -179,21 +179,48 @@ function codegenReactiveScope(
179
);
180
}
181
let firstOutputIndex: number | null = null;
182
- for (const output of scope.declarations) {
182
+ for (const declaration of scope.declarations) {
183
const index = cx.nextCacheIndex;
184
if (firstOutputIndex === null) {
185
firstOutputIndex = index;
186
}
187
188
invariant(
189
- output.name != null,
189
+ declaration.name != null,
190
"Expected identifier '@%s' to be named",
191
- output.id
191
+ declaration.id
192
);
193
194
- const name = convertIdentifier(output);
195
- cx.declare(output);
196
- statements.push(t.variableDeclaration("let", [t.variableDeclarator(name)]));
194
+ const name = convertIdentifier(declaration);
195
+ if (!cx.hasDeclared(declaration)) {
196
+ statements.push(
197
+ t.variableDeclaration("let", [t.variableDeclarator(name)])
198
+ );
199
+ }
200
+ cacheStoreStatements.push(
201
+ t.expressionStatement(
202
+ t.assignmentExpression(
203
+ "=",
204
+ t.memberExpression(t.identifier("$"), t.numericLiteral(index), true),
205
+ name
206
+ )
207
+ )
208
+ );
209
+ cacheLoadStatements.push(
210
+ t.expressionStatement(
211
+ t.assignmentExpression(
212
+ "=",
213
+ name,
214
+ t.memberExpression(t.identifier("$"), t.numericLiteral(index), true)
215
+ )
216
+ )
217
+ );
218
+ cx.declare(declaration);
219
+ }
220
+ for (const reassignment of scope.reassignments) {
221
+ const index = cx.nextCacheIndex;
222
+ const name = convertIdentifier(reassignment);
223
+
224
cacheStoreStatements.push(
225
t.expressionStatement(
226
t.assignmentExpression(
@@ -213,11 +240,6 @@ function codegenReactiveScope(
240
)
241
);
242
}
216
- invariant(
217
- firstOutputIndex !== null,
218
- "Expected scope '@%s' to have at least one output",
219
- scope.id
220
- );
243
let testCondition = (changeIdentifiers as Array<t.Expression>).reduce(
244
(acc: t.Expression | null, ident: t.Expression) => {
245
if (acc == null) {
@@ -228,6 +250,11 @@ function codegenReactiveScope(
250
null as t.Expression | null
251
);
252
if (testCondition === null) {
253
+ invariant(
254
+ firstOutputIndex !== null,
255
+ "Expected scope '@%s' to have at least one output",
256
+ scope.id
257
+ );
258
testCondition = t.binaryExpression(
259
"===",
260
t.memberExpression(
@@ -328,7 +355,7 @@ function codegenInstructionNullable(
355
value: t.Expression
356
): t.Statement | null {
357
let statement;
331
- if (instr.lvalue !== null && cx.declared(instr.lvalue.place.identifier)) {
358
+ if (instr.lvalue !== null && cx.hasDeclared(instr.lvalue.place.identifier)) {
359
statement = codegenInstruction(
360
cx,
361
{
compiler/forget/src/ReactiveScopes/InferReactiveScopeVariables.ts
+1
@@ -129,6 +129,7 @@ export function inferReactiveScopeVariables(fn: HIRFunction) {
129
range: identifier.mutableRange,
130
dependencies: new Set(),
131
declarations: new Set(),
132
+ reassignments: new Set(),
133
};
134
scopes.set(groupIdentifier, scope);
135
} else {
compiler/forget/src/ReactiveScopes/PrintReactiveFunction.ts
+4
-2
@@ -46,8 +46,10 @@ export function printReactiveBlock(
46
}] dependencies=[${Array.from(block.scope.dependencies)
47
.map((dep) => printDependency(dep))
48
.join(", ")}] declarations=[${Array.from(block.scope.declarations)
49
- .map((out) => printIdentifier(out))
50
- .join(", ")}] {`
49
+ .map((decl) => printIdentifier(decl))
50
+ .join(", ")}] reassignments=[${Array.from(block.scope.reassignments).map(
51
+ (reassign) => printIdentifier(reassign)
52
+ )}] {`
53
);
54
printReactiveInstructions(writer, block.instructions);
55
writer.writeLine("}");
compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts
+63
-22
@@ -33,12 +33,17 @@ import { eachReactiveValueOperand } from "./visitors";
33
export function propagateScopeDependencies(fn: ReactiveFunction): void {
34
const context = new Context(inferReactiveIdentifiers(fn));
35
if (fn.id !== null) {
36
- context.declare(fn.id, { kind: DeclKind.Const, id: makeInstructionId(0) });
36
+ context.declare(fn.id, {
37
+ kind: DeclKind.Const,
38
+ id: makeInstructionId(0),
39
+ scope: null,
40
+ });
41
}
42
for (const param of fn.params) {
43
context.declare(param.identifier, {
44
kind: DeclKind.Dynamic,
45
id: makeInstructionId(0),
46
+ scope: null,
47
});
48
}
49
visit(context, fn.body);
@@ -50,7 +55,11 @@ enum DeclKind {
55
}
56
57
type DeclMap = Map<Identifier, Decl>;
53
-type Decl = { kind: DeclKind; id: InstructionId };
58
+type Decl = {
59
+ kind: DeclKind;
60
+ id: InstructionId;
61
+ scope: ReactiveScope | null;
62
+};
63
64
type Scopes = Array<ReactiveScope>;
65
@@ -78,6 +87,12 @@ class Context {
87
return scopedDependencies;
88
}
89
90
+ /**
91
+ * Records where a value was declared, and optionally, the scope where the value originated from.
92
+ * This is later used to determine if a dependency should be added to a scope; if the current
93
+ * scope we are visiting is the same scope where the value originates, it can't be a dependency
94
+ * on itself.
95
+ */
96
declare(identifier: Identifier, decl: Decl): void {
97
this.#declarations.set(identifier, decl);
98
}
@@ -100,8 +115,8 @@ class Context {
115
return this.#scopes.indexOf(scope) !== -1;
116
}
117
103
- get #currentScope(): ReactiveScope {
104
- return this.#scopes.at(-1)!;
118
+ get currentScope(): ReactiveScope | null {
119
+ return this.#scopes.at(-1) ?? null;
120
}
121
122
isReactive(id: Identifier): boolean {
@@ -149,22 +164,26 @@ class Context {
164
165
// Any value used after its defining scope has concluded must be added as an
166
// output of its defining scope. Regardless of whether its a const or not,
152
- // some later code needs access to the value.
153
- if (decl !== undefined) {
154
- const operandScope = maybeDependency.place.identifier.scope;
155
- if (operandScope !== null && !this.#isScopeActive(operandScope)) {
156
- operandScope.declarations.add(maybeDependency.place.identifier);
157
- }
167
+ // some later code needs access to the value. If the current
168
+ // scope we are visiting is the same scope where the value originates,
169
+ // it can't be a dependency on itself.
170
+ if (
171
+ decl !== undefined &&
172
+ decl.scope !== null &&
173
+ !this.#isScopeActive(decl.scope)
174
+ ) {
175
+ decl.scope.declarations.add(maybeDependency.place.identifier);
176
}
177
178
// If this operand is used in a scope, has a dynamic value, and was defined
179
// before this scope, then its a dependency of the scope.
162
- const currentScope = this.#currentScope;
180
+ const currentScope = this.currentScope;
181
if (
182
+ currentScope != null &&
183
decl !== undefined &&
184
decl.kind !== DeclKind.Const &&
166
- currentScope !== undefined &&
167
- decl.id < currentScope.range.start
185
+ decl.id < currentScope.range.start &&
186
+ (decl.scope == null || !this.#isScopeActive(decl.scope))
187
) {
188
// Check if there is an existing dependency that describes this operand
189
for (const dep of this.#dependencies) {
@@ -201,6 +220,25 @@ class Context {
220
this.#dependencies.add(maybeDependency);
221
}
222
}
223
+
224
+ /**
225
+ * Record a variable that is declared in some other scope and that is being reassigned in the
226
+ * current one as a {@link ReactiveScope.reassignments}
227
+ */
228
+ visitReassignment(lvalue: LValue): void {
229
+ if (lvalue.kind !== InstructionKind.Reassign) {
230
+ return;
231
+ }
232
+ const declaration = this.#declarations.get(lvalue.place.identifier);
233
+ if (
234
+ this.currentScope != null &&
235
+ lvalue.place.identifier.scope != null &&
236
+ declaration !== undefined &&
237
+ declaration.scope !== lvalue.place.identifier.scope
238
+ ) {
239
+ this.currentScope.reassignments.add(lvalue.place.identifier);
240
+ }
241
+ }
242
}
243
244
function visit(context: Context, block: ReactiveBlock): void {
@@ -335,14 +373,17 @@ function visitInstructionValue(
373
function visitInstruction(context: Context, instr: ReactiveInstruction): void {
374
const { lvalue } = instr;
375
visitInstructionValue(context, instr.value, lvalue);
338
- if (lvalue !== null && lvalue.kind !== InstructionKind.Reassign) {
339
- // TODO: only assign Const if the value is never reassigned
340
- const kind = context.isReactive(lvalue.place.identifier)
341
- ? DeclKind.Dynamic
342
- : DeclKind.Const;
343
- context.declare(lvalue.place.identifier, {
344
- kind,
345
- id: lvalue.place.identifier.mutableRange.start,
346
- });
376
+ if (lvalue == null) {
377
+ return;
378
}
379
+ context.visitReassignment(lvalue);
380
+ // TODO: only assign Const if the value is never reassigned
381
+ const kind = context.isReactive(lvalue.place.identifier)
382
+ ? DeclKind.Dynamic
383
+ : DeclKind.Const;
384
+ context.declare(lvalue.place.identifier, {
385
+ kind,
386
+ id: lvalue.place.identifier.mutableRange.start,
387
+ scope: context.currentScope,
388
+ });
389
}
compiler/forget/src/ReactiveScopes/PruneUnusedScopes.ts
+7
-1
@@ -30,7 +30,13 @@ function visitBlock(block: ReactiveBlock): ReactiveBlock {
30
}
31
case "scope": {
32
stmt.instructions = visitBlock(stmt.instructions);
33
- if (stmt.scope.declarations.size === 0) {
33
+ // If a scope doesn't have declarations but reassigns a value, the scope shouldn't be pruned
34
+ // as we still want to generate a memo block for that scope
35
+ if (
36
+ stmt.scope.declarations.size === 0 &&
37
+ (stmt.scope.dependencies.size === 0 ||
38
+ stmt.scope.reassignments.size === 0)
39
+ ) {
40
nextBlock ??= block.slice(0, i);
41
nextBlock.push(...stmt.instructions);
42
continue;
compiler/forget/src/__tests__/fixtures/hir/ssa-leave-case.expect.md
+9
-6
@@ -27,10 +27,11 @@ function Component(props) {
27
const c_0 = $[0] !== props.p0;
28
const c_1 = $[1] !== props.p1;
29
let x;
30
+ let y$0;
31
if (c_0 || c_1) {
32
x = [];
33
const y = undefined;
33
- let y$0 = y;
34
+ y$0 = y;
35
if (props.p0) {
36
x.push(props.p1);
37
const y$1 = x;
@@ -39,22 +40,24 @@ function Component(props) {
40
$[0] = props.p0;
41
$[1] = props.p1;
42
$[2] = x;
43
+ $[3] = y$0;
44
} else {
45
x = $[2];
46
+ y$0 = $[3];
47
}
45
- const c_3 = $[3] !== x;
48
+ const c_4 = $[4] !== x;
49
let t0;
47
- if (c_3) {
50
+ if (c_4) {
51
t0 = (
52
<Component>
53
{x}
54
{y$0}
55
</Component>
56
);
54
- $[3] = x;
55
- $[4] = t0;
57
+ $[4] = x;
58
+ $[5] = t0;
59
} else {
57
- t0 = $[4];
60
+ t0 = $[5];
61
}
62
return t0;
63
}
compiler/forget/src/__tests__/fixtures/hir/switch-non-final-default.expect.md
+17
-14
@@ -36,10 +36,11 @@ function Component(props) {
36
const c_0 = $[0] !== props.p0;
37
const c_1 = $[1] !== props.p2;
38
let x;
39
+ let y$0;
40
if (c_0 || c_1) {
41
x = [];
42
const y = undefined;
42
- let y$0 = y;
43
+ y$0 = y;
44
bb1: switch (props.p0) {
45
case 1: {
46
break bb1;
@@ -47,11 +48,11 @@ function Component(props) {
48
case true: {
49
x.push(props.p2);
50
let y$1;
50
- if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
51
+ if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
52
y$1 = [];
52
- $[3] = y$1;
53
+ $[4] = y$1;
54
} else {
54
- y$1 = $[3];
55
+ y$1 = $[4];
56
}
57
y$0 = y$1;
58
break bb1;
@@ -67,27 +68,29 @@ function Component(props) {
68
$[0] = props.p0;
69
$[1] = props.p2;
70
$[2] = x;
71
+ $[3] = y$0;
72
} else {
73
x = $[2];
74
+ y$0 = $[3];
75
}
73
- const c_4 = $[4] !== x;
76
+ const c_5 = $[5] !== x;
77
let child;
75
- if (c_4) {
78
+ if (c_5) {
79
child = <Component data={x}></Component>;
77
- $[4] = x;
78
- $[5] = child;
80
+ $[5] = x;
81
+ $[6] = child;
82
} else {
80
- child = $[5];
83
+ child = $[6];
84
}
85
y$0.push(props.p4);
83
- const c_6 = $[6] !== child;
86
+ const c_7 = $[7] !== child;
87
let t0;
85
- if (c_6) {
88
+ if (c_7) {
89
t0 = <Component data={y$0}>{child}</Component>;
87
- $[6] = child;
88
- $[7] = t0;
90
+ $[7] = child;
91
+ $[8] = t0;
92
} else {
90
- t0 = $[7];
93
+ t0 = $[8];
94
}
95
return t0;
96
}
compiler/forget/src/__tests__/fixtures/hir/switch.expect.md
+14
-11
@@ -32,10 +32,11 @@ function Component(props) {
32
const c_1 = $[1] !== props.p2;
33
const c_2 = $[2] !== props.p3;
34
let x;
35
+ let y$0;
36
if (c_0 || c_1 || c_2) {
37
x = [];
38
const y = undefined;
38
- let y$0 = y;
39
+ y$0 = y;
40
switch (props.p0) {
41
case true: {
42
x.push(props.p2);
@@ -50,27 +51,29 @@ function Component(props) {
51
$[1] = props.p2;
52
$[2] = props.p3;
53
$[3] = x;
54
+ $[4] = y$0;
55
} else {
56
x = $[3];
57
+ y$0 = $[4];
58
}
56
- const c_4 = $[4] !== x;
59
+ const c_5 = $[5] !== x;
60
let child;
58
- if (c_4) {
61
+ if (c_5) {
62
child = <Component data={x}></Component>;
60
- $[4] = x;
61
- $[5] = child;
63
+ $[5] = x;
64
+ $[6] = child;
65
} else {
63
- child = $[5];
66
+ child = $[6];
67
}
68
y$0.push(props.p4);
66
- const c_6 = $[6] !== child;
69
+ const c_7 = $[7] !== child;
70
let t0;
68
- if (c_6) {
71
+ if (c_7) {
72
t0 = <Component data={y$0}>{child}</Component>;
70
- $[6] = child;
71
- $[7] = t0;
73
+ $[7] = child;
74
+ $[8] = t0;
75
} else {
73
- t0 = $[7];
76
+ t0 = $[8];
77
}
78
return t0;
79
}