@samitouri / QOS-React-1 / commits / d1355179d7

Improved check for intermediate values for merged scopes

We can't merge scopes if the intervening instructions produce values that are used later on. This PR improves the mechanism for detecting this case: first we build up a mapping of the last time (max instruction id) each identifier is used. Then when we're about to merge scopes we check if all the intervening lvalues are last used at or before the scope. If so that means it's safe to merge. I can't think of any edge cases that are problematic in the old behavior, but this version is more trivially correct and should allow us to extend to other types of instructions more easily.

Joe Savona committed Sep 27, 2023 at 14:04 UTC d1355179d7cedfd213d9eb5bf35395c69d5443ef
1 file changed +43 -48
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/MergeConsecutiveScopes.ts
+43 -48
@@ -14,8 +14,8 @@ import {
14 ReactiveScope,
15 ReactiveScopeBlock,
16 ReactiveScopeDependency,
17 + makeInstructionId,
18 } from "../HIR";
18 -import { assertExhaustive } from "../Utils/utils";
19 import {
20 ReactiveFunctionTransform,
21 ReactiveFunctionVisitor,
@@ -52,10 +52,36 @@ import {
52 *
53 */
54 export function mergeConsecutiveScopes(fn: ReactiveFunction): void {
55 - visitReactiveFunction(fn, new Transform(), undefined);
55 + const lastUsageVisitor = new FindLastUsageVisitor();
56 + visitReactiveFunction(fn, lastUsageVisitor, undefined);
57 + visitReactiveFunction(
58 + fn,
59 + new Transform(lastUsageVisitor.lastUsage),
60 + undefined
61 + );
62 +}
63 +
64 +class FindLastUsageVisitor extends ReactiveFunctionVisitor<void> {
65 + lastUsage: Map<IdentifierId, InstructionId> = new Map();
66 +
67 + override visitPlace(id: InstructionId, place: Place, _state: void): void {
68 + const previousUsage = this.lastUsage.get(place.identifier.id);
69 + const lastUsage =
70 + previousUsage !== undefined
71 + ? makeInstructionId(Math.max(previousUsage, id))
72 + : id;
73 + this.lastUsage.set(place.identifier.id, lastUsage);
74 + }
75 }
76
77 class Transform extends ReactiveFunctionTransform<void> {
78 + lastUsage: Map<IdentifierId, InstructionId>;
79 +
80 + constructor(lastUsage: Map<IdentifierId, InstructionId>) {
81 + super();
82 + this.lastUsage = lastUsage;
83 + }
84 +
85 override visitBlock(block: ReactiveBlock, state: void): void {
86 this.traverseBlock(block, state);
87
@@ -140,7 +166,7 @@ class Transform extends ReactiveFunctionTransform<void> {
166 // if those intermediate instructions are all used by the second scope.
167 // if not, merging them would make those values unavailable to subsequent
168 // code by moving them inside a different block scope in the output.
143 - usesAllLvalues(instr.instructions, lvalues)
169 + areLValuesLastUsedByScope(instr.scope, lvalues, this.lastUsage)
170 ) {
171 const intermediateInstructions = block.slice(currentScope.to, i);
172 currentScope.scope.instructions.push(...intermediateInstructions);
@@ -173,54 +199,23 @@ class Transform extends ReactiveFunctionTransform<void> {
199 }
200 }
201
176 -function usesAllLvalues(
177 - block: ReactiveBlock,
178 - lvalues: Set<IdentifierId>
202 +/**
203 + * Returns whether the given @param scope is the last usage of all
204 + * the given @param lvalues. Returns false if any of the lvalues
205 + * are used again after the scope.
206 + */
207 +function areLValuesLastUsedByScope(
208 + scope: ReactiveScope,
209 + lvalues: Set<IdentifierId>,
210 + lastUsage: Map<IdentifierId, InstructionId>
211 ): boolean {
180 - if (lvalues.size === 0) {
181 - return true;
182 - }
183 - const visitor = new OperandVisitor();
184 - visitor.traverseBlock(block, lvalues);
185 - return lvalues.size === 0;
186 -}
187 -
188 -class OperandVisitor extends ReactiveFunctionVisitor<Set<IdentifierId>> {
189 - override visitPlace(
190 - _id: InstructionId,
191 - place: Place,
192 - state: Set<IdentifierId>
193 - ): void {
194 - state.delete(place.identifier.id);
195 - }
196 -
197 - override traverseBlock(block: ReactiveBlock, state: Set<IdentifierId>): void {
198 - for (const instr of block) {
199 - if (state.size === 0) {
200 - return;
201 - }
202 - switch (instr.kind) {
203 - case "instruction": {
204 - this.visitInstruction(instr.instruction, state);
205 - break;
206 - }
207 - case "scope": {
208 - this.visitScope(instr, state);
209 - break;
210 - }
211 - case "terminal": {
212 - this.visitTerminal(instr, state);
213 - break;
214 - }
215 - default: {
216 - assertExhaustive(
217 - instr,
218 - `Unexpected instruction kind '${(instr as any).kind}'`
219 - );
220 - }
221 - }
212 + for (const lvalue of lvalues) {
213 + const lastUsedAt = lastUsage.get(lvalue)!;
214 + if (lastUsedAt >= scope.range.end) {
215 + return false;
216 }
217 }
218 + return true;
219 }
220
221 function canMergeScopes(a: ReactiveScope, b: ReactiveScope): boolean {