@samitouri / QOS-React-2 / commits / d47f608c61

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 }