@samitouri / QOS-React-2 / commits / 1fd4bad525

RHS member expression converts to PropertyLoad

This is an incremental step to removing `Place.memberPath`. This PR changes how we handle MemberExpressions in rvalue position, converting to a new `PropertyLoad` InstructionValue variant. Example: ``` let x = a.b; x.y = b.c; => Const tmp1 = PropertyLoad a, 'b'; Const x = Place tmp1; Const tmp2 = PropertyLoad b, 'c'; Reassign x.y = tmp2 ``` That we already recently made a chance to ensure that _if_ the lvalue is a member expression, that we convert the RHS to a Place. So although `x.y = b.c` could technically be lowered to a single instruction (with the`b.c` as a PropertyLoad), we force this to a temporary to ensure that we can independently memoize the RHS value. The net result is that the following combinations are possible: * `x = y`, lvalue identifier, rvalue identifier * `x.y = y` lvalue member path, rvalue identifier * `x = y.z` lvalue identifier, rvalue property load As noted above, `x.y = a.b` no longer occurs (and there's an invariant for this in one of the passes). A follow-up PR will add a PropertyStore instruction so that we can remove member paths in lvalue position too.

Joe Savona committed Dec 22, 2022 at 14:00 UTC 1fd4bad52589ea750c2a54ecddc314166e2f6e4e
25 files changed +486 -212
compiler/forget/src/CompilerPipeline.ts
+13 -1
@@ -27,7 +27,7 @@ import {
27 renameVariables,
28 } from "./ReactiveScopes";
29 import { eliminateRedundantPhi, enterSSA, leaveSSA } from "./SSA";
30 -import { logHIRFunction } from "./Utils/logger";
30 +import { logHIRFunction, logReactiveFunction } from "./Utils/logger";
31
32 export type CompilerResult = {
33 ast: t.Function;
@@ -68,11 +68,23 @@ export default function (
68 logHIRFunction("inferReactiveScopes", ir);
69
70 const reactiveFunction = buildReactiveFunction(ir);
71 + logReactiveFunction("buildReactiveFunction", reactiveFunction);
72 +
73 pruneUnusedLabels(reactiveFunction);
74 + logReactiveFunction("pruneUnusedLabels", reactiveFunction);
75 +
76 flattenReactiveLoops(reactiveFunction);
77 + logReactiveFunction("flattenReactiveLoops", reactiveFunction);
78 +
79 propagateScopeDependencies(reactiveFunction);
80 + logReactiveFunction("propagateScopeDependencies", reactiveFunction);
81 +
82 pruneUnusedScopes(reactiveFunction);
83 + logReactiveFunction("pruneUnusedScopes", reactiveFunction);
84 +
85 renameVariables(reactiveFunction);
86 + logReactiveFunction("renameVariables", reactiveFunction);
87 +
88 const ast = codegenReactiveFunction(reactiveFunction);
89
90 return {
compiler/forget/src/HIR/BuildHIR.ts
+24 -6
@@ -981,11 +981,17 @@ function lowerExpression(
981 `Unhandled assignment operator '${operator}'`
982 );
983
984 - const left = lowerLVal(builder, expr.get("left"));
984 + const lvalue = lowerLVal(builder, expr.get("left"));
985 + const leftPath = expr.get("left");
986 + invariant(
987 + leftPath.isIdentifier() || leftPath.isMemberExpression(),
988 + "Expected assignment expression lvalue to be an identifier or member expression"
989 + );
990 + const left = lowerExpressionToPlace(builder, leftPath);
991 const right = lowerExpressionToPlace(builder, expr.get("right"));
992 builder.push({
993 id: makeInstructionId(0),
988 - lvalue: { place: left, kind: InstructionKind.Reassign },
994 + lvalue: { place: lvalue, kind: InstructionKind.Reassign },
995 value: {
996 kind: "BinaryExpression",
997 operator: binaryOperator,
@@ -995,7 +1001,7 @@ function lowerExpression(
1001 },
1002 loc: exprLoc,
1003 });
998 - return left;
1004 + return lvalue;
1005 }
1006 case "MemberExpression": {
1007 const expr = exprPath as NodePath<t.MemberExpression>;
@@ -1006,13 +1012,25 @@ function lowerExpression(
1012 property.isIdentifier(),
1013 "Handle non-identifier properties"
1014 );
1015 + const value: InstructionValue = {
1016 + kind: "PropertyLoad",
1017 + object,
1018 + property: property.node.name,
1019 + loc: exprLoc,
1020 + };
1021 const place: Place = {
1022 kind: "Identifier",
1011 - identifier: object.identifier,
1012 - memberPath: [...(object.memberPath ?? []), property.node.name],
1013 - effect: Effect.Unknown,
1023 + identifier: builder.makeTemporary(),
1024 + memberPath: null,
1025 + effect: Effect.Read,
1026 loc: exprLoc,
1027 };
1028 + builder.push({
1029 + id: makeInstructionId(0),
1030 + lvalue: { place: { ...place }, kind: InstructionKind.Const },
1031 + value,
1032 + loc: exprLoc,
1033 + });
1034 return place;
1035 }
1036 case "JSXElement": {
compiler/forget/src/HIR/Codegen.ts
+7
@@ -458,6 +458,13 @@ export function codegenInstructionValue(
458 value = node;
459 break;
460 }
461 + case "PropertyLoad": {
462 + value = t.memberExpression(
463 + codegenPlace(temp, instrValue.object),
464 + t.identifier(instrValue.property)
465 + );
466 + break;
467 + }
468 case "Identifier": {
469 value = codegenPlace(temp, instrValue);
470 break;
compiler/forget/src/HIR/HIR.ts
+5
@@ -293,6 +293,11 @@ export type InstructionData =
293 | { kind: "ArrayExpression"; elements: Array<Place> }
294 | { kind: "JsxFragment"; children: Array<Place> }
295
296 + // store `object.property = value`
297 + // | { kind: "PropertyStore"; object: Place; property: string; value: Place }
298 + // load `object.property`
299 + | { kind: "PropertyLoad"; object: Place; property: string }
300 +
301 /**
302 * Catch-all for statements such as type imports, nested class declarations, etc
303 * which are not directly represented, but included for completeness and to allow
compiler/forget/src/HIR/InferAlias.ts
+4
@@ -35,6 +35,10 @@ function inferInstr(instr: Instruction, state: AliasAnalyser) {
35 alias = instrValue;
36 break;
37 }
38 + case "PropertyLoad": {
39 + alias = instrValue.object;
40 + break;
41 + }
42 default:
43 return;
44 }
compiler/forget/src/HIR/InferMutableRanges.ts
+11 -1
@@ -1,3 +1,10 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 import { HIRFunction } from "./HIR";
9 import { inferAliases } from "./InferAlias";
10 import { inferAliasForStores } from "./InferAliasForStores";
@@ -11,6 +18,9 @@ export function inferMutableRanges(ir: HIRFunction) {
18 // Calculate aliases
19 const aliases = inferAliases(ir);
20 let size = aliases.size;
21 + // Eagerly canonicalize so that if nothing changes we can bail out
22 + // after a single iteration
23 + aliases.canonicalize();
24 do {
25 size = aliases.size;
26 // Infer mutable ranges for aliases that are not fields
@@ -18,7 +28,7 @@ export function inferMutableRanges(ir: HIRFunction) {
28
29 // Update aliasing information of fields
30 inferAliasForStores(ir, aliases);
21 - } while (aliases.size > size);
31 + } while (aliases.size > size || !aliases.canonicalize());
32
33 // Re-infer mutable ranges for all values
34 inferMutableLifetimes(ir, true);
compiler/forget/src/HIR/InferReferenceEffects.ts
+32 -6
@@ -229,6 +229,10 @@ class Environment {
229 this.#variables.set(place.identifier.id, new Set([value]));
230 }
231
232 + isDefined(place: Place): boolean {
233 + return this.#variables.has(place.identifier.id);
234 + }
235 +
236 /**
237 * Records that a given Place was accessed with the given kind and:
238 * - Updates the effect of @param place based on the kind of value
@@ -571,7 +575,35 @@ function inferBlock(env: Environment, block: BasicBlock) {
575 valueKind = ValueKind.Immutable;
576 break;
577 }
578 + case "PropertyLoad": {
579 + if (!env.isDefined(instrValue.object)) {
580 + // TODO @josephsavona: improve handling of globals
581 + const value: InstructionValue = {
582 + kind: "Primitive",
583 + loc: instrValue.loc,
584 + value: undefined,
585 + };
586 + env.initialize(value, ValueKind.Frozen);
587 + env.define(instrValue.object, value);
588 + }
589 +
590 + env.reference(instrValue.object, Effect.Read);
591 + const lvalue = instr.lvalue;
592 + if (lvalue !== null) {
593 + invariant(
594 + lvalue.place.memberPath === null,
595 + "PropertyLoad must always be saved to a temporary"
596 + );
597 + env.initialize(instrValue, env.kind(instrValue.object));
598 + env.define(lvalue.place, instrValue);
599 + }
600 + continue;
601 + }
602 case "Identifier": {
603 + invariant(
604 + instrValue.memberPath === null,
605 + "Expected RHS memberPath to be lowered to PropertyLoad"
606 + );
607 env.reference(instrValue, Effect.Read);
608 const lvalue = instr.lvalue;
609 if (lvalue !== null) {
@@ -582,14 +614,8 @@ function inferBlock(env: Environment, block: BasicBlock) {
614 ) {
615 // direct aliasing: `a = b`;
616 env.alias(lvalue.place, instrValue);
585 - } else if (lvalue.place.memberPath === null) {
586 - // redefine lvalue: `a = b.c.d`
587 - env.initialize(instrValue, env.kind(instrValue));
588 - env.define(lvalue.place, instrValue);
617 } else {
618 // no-op: `a.b.c = d`
591 - // or
592 - // no-op: `a.b.c = d.e.f`
619 const effect = isObjectType(lvalue.place.identifier)
620 ? Effect.Store
621 : Effect.Mutate;
compiler/forget/src/HIR/PrintHIR.ts
+6 -1
@@ -265,6 +265,12 @@ export function printInstructionValue(instrValue: InstructionValue): string {
265 value = printPlace(instrValue);
266 break;
267 }
268 + case "PropertyLoad": {
269 + value = `PropertyLoad ${printPlace(instrValue.object)}.${
270 + instrValue.property
271 + }`;
272 + break;
273 + }
274 default: {
275 assertExhaustive(
276 instrValue,
@@ -291,7 +297,6 @@ function printMutableRange(identifier: Identifier): string {
297
298 export function printLValue(lval: LValue): string {
299 let place = printPlace(lval.place);
294 - place += printMutableRange(lval.place.identifier);
300 switch (lval.kind) {
301 case InstructionKind.Let: {
302 return `Let ${place}`;
compiler/forget/src/HIR/visitors.ts
+8
@@ -41,6 +41,10 @@ export function* eachInstructionValueOperand(
41 yield instrValue;
42 break;
43 }
44 + case "PropertyLoad": {
45 + yield instrValue.object;
46 + break;
47 + }
48 case "UnaryExpression": {
49 yield instrValue.value;
50 break;
@@ -92,6 +96,10 @@ export function mapInstructionOperands(
96 instrValue.right = fn(instrValue.right);
97 break;
98 }
99 + case "PropertyLoad": {
100 + instrValue.object = fn(instrValue.object);
101 + break;
102 + }
103 case "Identifier": {
104 instr.value = fn(instrValue);
105 break;
compiler/forget/src/ReactiveScopes/InferReactiveScopeVariables.ts
+1
@@ -164,6 +164,7 @@ function mayAllocate(value: InstructionValue): boolean {
164 switch (value.kind) {
165 case "BinaryExpression":
166 case "Identifier":
167 + case "PropertyLoad":
168 case "JSXText":
169 case "Primitive": {
170 return false;
compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts
+164 -130
@@ -11,6 +11,7 @@ import {
11 InstructionId,
12 InstructionKind,
13 InstructionValue,
14 + LValue,
15 makeInstructionId,
16 Place,
17 ReactiveBlock,
@@ -19,6 +20,7 @@ import {
20 ReactiveValueBlock,
21 } from "../HIR/HIR";
22 import { eachInstructionValueOperand } from "../HIR/visitors";
23 +import { invariant } from "../Utils/CompilerError";
24 import { assertExhaustive } from "../Utils/utils";
25
26 /**
@@ -28,18 +30,17 @@ import { assertExhaustive } from "../Utils/utils";
30 * their direct dependencies and those of their child scopes.
31 */
32 export function propagateScopeDependencies(fn: ReactiveFunction): void {
31 - const dependencies: Set<Place> = new Set();
32 - const declarations: DeclMap = new Map();
33 + const context = new Context();
34 if (fn.id !== null) {
34 - declarations.set(fn.id, { kind: DeclKind.Const, id: makeInstructionId(0) });
35 + context.declare(fn.id, { kind: DeclKind.Const, id: makeInstructionId(0) });
36 }
37 for (const param of fn.params) {
37 - declarations.set(param.identifier, {
38 + context.declare(param.identifier, {
39 kind: DeclKind.Dynamic,
40 id: makeInstructionId(0),
41 });
42 }
42 - visit(fn.body, dependencies, declarations, []);
43 + visit(context, fn.body);
44 }
45
46 enum DeclKind {
@@ -47,40 +48,151 @@ enum DeclKind {
48 Dynamic = "Dynamic",
49 }
50
50 -type DeclMap = Map<Identifier, { kind: DeclKind; id: InstructionId }>;
51 +type DeclMap = Map<Identifier, Decl>;
52 +type Decl = { kind: DeclKind; id: InstructionId };
53
54 type Scopes = Array<ReactiveScope>;
55
54 -function visit(
55 - block: ReactiveBlock,
56 - dependencies: Set<Place>,
57 - declarations: DeclMap,
58 - scopes: Scopes
59 -): void {
56 +class Context {
57 + #declarations: DeclMap = new Map();
58 + #dependencies: Set<Place> = new Set();
59 + #properties: Map<Identifier, Place> = new Map();
60 + #scopes: Scopes = [];
61 +
62 + enter(scope: ReactiveScope, fn: () => void): Set<Place> {
63 + const previousDependencies = this.#dependencies;
64 + const scopedDependencies = new Set<Place>();
65 + this.#dependencies = scopedDependencies;
66 + this.#scopes.push(scope);
67 + fn();
68 + this.#scopes.pop();
69 + this.#dependencies = previousDependencies;
70 + return scopedDependencies;
71 + }
72 +
73 + declare(identifier: Identifier, decl: Decl): void {
74 + this.#declarations.set(identifier, decl);
75 + }
76 +
77 + declareProperty(lvalue: Place, object: Place, property: string): void {
78 + invariant(
79 + lvalue.memberPath === null,
80 + "Expected property loads to be stored to a temporary (no member path)"
81 + );
82 + invariant(
83 + object.memberPath === null,
84 + "Expected operands to have null memberPath"
85 + );
86 + const objectPlace = this.#properties.get(object.identifier);
87 + let place: Place;
88 + if (objectPlace === undefined) {
89 + place = { ...object, memberPath: [property] };
90 + } else {
91 + place = {
92 + ...objectPlace,
93 + memberPath: [...(objectPlace.memberPath ?? []), property],
94 + };
95 + }
96 + this.#properties.set(lvalue.identifier, place);
97 + }
98 +
99 + #isScopeActive(scope: ReactiveScope): boolean {
100 + return this.#scopes.indexOf(scope) !== -1;
101 + }
102 +
103 + get #currentScope(): ReactiveScope {
104 + return this.#scopes.at(-1)!;
105 + }
106 +
107 + visitOperand(operand: Place): void {
108 + let maybeDependency: Place;
109 + if (operand.memberPath !== null) {
110 + // Operands may have memberPaths when propagating depenencies of an inner scope upward
111 + // In this case we use the dependency as-is
112 + maybeDependency = operand;
113 + } else {
114 + // Otherwise if this operand is a temporary created for a property load, resolve it to
115 + // the expanded Place. Fall back to using the operand as-is.
116 + maybeDependency = this.#properties.get(operand.identifier) ?? operand;
117 + }
118 +
119 + const decl = this.#declarations.get(maybeDependency.identifier);
120 +
121 + // Any value used after its defining scope has concluded must be added as an
122 + // output of its defining scope. Regardless of whether its a const or not,
123 + // some later code needs access to the value.
124 + if (decl !== undefined) {
125 + const operandScope = maybeDependency.identifier.scope;
126 + if (operandScope !== null && !this.#isScopeActive(operandScope)) {
127 + operandScope.outputs.add(maybeDependency.identifier);
128 + }
129 + }
130 +
131 + // If this operand is used in a scope, has a dynamic value, and was defined
132 + // before this scope, then its a dependency of the scope.
133 + const currentScope = this.#currentScope;
134 + if (
135 + decl !== undefined &&
136 + decl.kind !== DeclKind.Const &&
137 + currentScope !== undefined &&
138 + decl.id < currentScope.range.start
139 + ) {
140 + // Check if there is an existing dependency that describes this operand
141 + for (const dep of this.#dependencies) {
142 + // not the same identifier
143 + if (dep.identifier !== maybeDependency.identifier) {
144 + continue;
145 + }
146 + const depPath = dep.memberPath;
147 + // existing dep covers all paths
148 + if (depPath === null) {
149 + return;
150 + }
151 + const operandPath = maybeDependency.memberPath;
152 + // existing dep is for a path, this operand covers all paths so swap them
153 + if (operandPath === null) {
154 + this.#dependencies.delete(dep);
155 + this.#dependencies.add(maybeDependency);
156 + return;
157 + }
158 + // both the operand and dep have paths, determine if the existing path
159 + // is a subset of the new path
160 + let commonPathIndex = 0;
161 + while (
162 + commonPathIndex < operandPath.length &&
163 + commonPathIndex < depPath.length &&
164 + operandPath[commonPathIndex] === depPath[commonPathIndex]
165 + ) {
166 + commonPathIndex++;
167 + }
168 + if (commonPathIndex === depPath.length) {
169 + return;
170 + }
171 + }
172 + this.#dependencies.add(maybeDependency);
173 + }
174 + }
175 +}
176 +
177 +function visit(context: Context, block: ReactiveBlock): void {
178 for (const item of block) {
179 switch (item.kind) {
180 case "scope": {
63 - const scopeDependencies: Set<Place> = new Set();
64 - // TODO: it would be sufficient to use a single mapping of declarations
65 - const scopeDeclarations: DeclMap = new Map(declarations);
66 - scopes.push(item.scope);
67 - visit(item.instructions, scopeDependencies, scopeDeclarations, scopes);
68 - scopes.pop();
181 + const scopeDependencies = context.enter(item.scope, () => {
182 + visit(context, item.instructions);
183 + });
184 item.scope.dependencies = scopeDependencies;
185 for (const dep of scopeDependencies) {
186 // propagate dependencies upward using the same rules as
187 // normal dependency collection. child scopes may have dependencies
188 // on values created within the outer scope, which necessarily cannot
189 // be dependencies of the outer scope
75 - visitOperand(dep, dependencies, declarations, scopes);
76 - }
77 - for (const [ident, kind] of scopeDeclarations) {
78 - declarations.set(ident, kind);
190 + context.visitOperand(dep);
191 }
192 break;
193 }
194 case "instruction": {
83 - visitInstruction(item.instruction, dependencies, declarations, scopes);
195 + visitInstruction(context, item.instruction);
196 break;
197 }
198 case "terminal": {
@@ -92,44 +204,39 @@ function visit(
204 }
205 case "return": {
206 if (terminal.value !== null) {
95 - visitOperand(terminal.value, dependencies, declarations, scopes);
207 + context.visitOperand(terminal.value);
208 }
209 break;
210 }
211 case "throw": {
100 - visitOperand(terminal.value, dependencies, declarations, scopes);
212 + context.visitOperand(terminal.value);
213 break;
214 }
215 case "for": {
104 - visitValueBlock(terminal.init, dependencies, declarations, scopes);
105 - visitValueBlock(terminal.test, dependencies, declarations, scopes);
106 - visitValueBlock(
107 - terminal.update,
108 - dependencies,
109 - declarations,
110 - scopes
111 - );
112 - visit(terminal.loop, dependencies, declarations, scopes);
216 + visitValueBlock(context, terminal.init);
217 + visitValueBlock(context, terminal.test);
218 + visitValueBlock(context, terminal.update);
219 + visit(context, terminal.loop);
220 break;
221 }
222 case "while": {
116 - visitValueBlock(terminal.test, dependencies, declarations, scopes);
117 - visit(terminal.loop, dependencies, declarations, scopes);
223 + visitValueBlock(context, terminal.test);
224 + visit(context, terminal.loop);
225 break;
226 }
227 case "if": {
121 - visitOperand(terminal.test, dependencies, declarations, scopes);
122 - visit(terminal.consequent, dependencies, declarations, scopes);
228 + context.visitOperand(terminal.test);
229 + visit(context, terminal.consequent);
230 if (terminal.alternate !== null) {
124 - visit(terminal.alternate, dependencies, declarations, scopes);
231 + visit(context, terminal.alternate);
232 }
233 break;
234 }
235 case "switch": {
129 - visitOperand(terminal.test, dependencies, declarations, scopes);
236 + context.visitOperand(terminal.test);
237 for (const case_ of terminal.cases) {
238 if (case_.block !== undefined) {
132 - visit(case_.block, dependencies, declarations, scopes);
239 + visit(context, case_.block);
240 }
241 }
242 break;
@@ -150,95 +257,21 @@ function visit(
257 }
258 }
259
153 -function visitValueBlock(
154 - block: ReactiveValueBlock,
155 - dependencies: Set<Place>,
156 - declarations: DeclMap,
157 - scopes: Scopes
158 -): void {
260 +function visitValueBlock(context: Context, block: ReactiveValueBlock): void {
261 for (const initItem of block.instructions) {
262 if (initItem.kind === "instruction") {
161 - visitInstruction(
162 - initItem.instruction,
163 - dependencies,
164 - declarations,
165 - scopes
166 - );
263 + visitInstruction(context, initItem.instruction);
264 }
265 }
266 if (block.value !== null) {
170 - visitInstructionValue(block.value, dependencies, declarations, scopes);
171 - }
172 -}
173 -
174 -function visitOperand(
175 - maybeDependency: Place,
176 - dependencies: Set<Place>,
177 - declarations: DeclMap,
178 - scopes: Scopes
179 -): void {
180 - const decl = declarations.get(maybeDependency.identifier);
181 -
182 - // Any value used after its defining scope has concluded must be added as an
183 - // output of its defining scope. Regardless of whether its a const or not,
184 - // some later code needs access to the value.
185 - if (decl !== undefined) {
186 - const operandScope = maybeDependency.identifier.scope;
187 - if (operandScope !== null && scopes.indexOf(operandScope) === -1) {
188 - operandScope.outputs.add(maybeDependency.identifier);
189 - }
190 - }
191 -
192 - // If this operand is used in a scope, has a dynamic value, and was defined
193 - // before this scope, then its a dependency of the scope.
194 - const currentScope = scopes.at(-1);
195 - if (
196 - decl !== undefined &&
197 - decl.kind !== DeclKind.Const &&
198 - currentScope !== undefined &&
199 - decl.id < currentScope.range.start
200 - ) {
201 - // Check if there is an existing dependency that describes this operand
202 - for (const dep of dependencies) {
203 - // not the same identifier
204 - if (dep.identifier !== maybeDependency.identifier) {
205 - continue;
206 - }
207 - const depPath = dep.memberPath;
208 - // existing dep covers all paths
209 - if (depPath === null) {
210 - return;
211 - }
212 - const operandPath = maybeDependency.memberPath;
213 - // existing dep is for a path, this operand covers all paths so swap them
214 - if (operandPath === null) {
215 - dependencies.delete(dep);
216 - dependencies.add(maybeDependency);
217 - return;
218 - }
219 - // both the operand and dep have paths, determine if the existing path
220 - // is a subset of the new path
221 - let commonPathIndex = 0;
222 - while (
223 - commonPathIndex < operandPath.length &&
224 - commonPathIndex < depPath.length &&
225 - operandPath[commonPathIndex] === depPath[commonPathIndex]
226 - ) {
227 - commonPathIndex++;
228 - }
229 - if (commonPathIndex === depPath.length) {
230 - return;
231 - }
232 - }
233 - dependencies.add(maybeDependency);
267 + visitInstructionValue(context, block.value, null);
268 }
269 }
270
271 function visitInstructionValue(
272 + context: Context,
273 value: InstructionValue,
239 - dependencies: Set<Place>,
240 - declarations: DeclMap,
241 - scopes: Scopes
274 + lvalue: LValue | null
275 ): void {
276 for (const operand of eachInstructionValueOperand(value)) {
277 // check for method invocation, we want to depend on the callee, not the method
@@ -251,21 +284,18 @@ function visitInstructionValue(
284 ...operand,
285 memberPath: operand.memberPath.slice(0, -1),
286 };
254 - visitOperand(callee, dependencies, declarations, scopes);
287 + context.visitOperand(callee);
288 + } else if (value.kind === "PropertyLoad" && lvalue !== null) {
289 + context.declareProperty(lvalue.place, value.object, value.property);
290 } else {
256 - visitOperand(operand, dependencies, declarations, scopes);
291 + context.visitOperand(operand);
292 }
293 }
294 }
295
261 -function visitInstruction(
262 - instr: Instruction,
263 - dependencies: Set<Place>,
264 - declarations: DeclMap,
265 - scopes: Scopes
266 -): void {
267 - visitInstructionValue(instr.value, dependencies, declarations, scopes);
296 +function visitInstruction(context: Context, instr: Instruction): void {
297 const { lvalue } = instr;
298 + visitInstructionValue(context, instr.value, lvalue);
299 if (
300 lvalue !== null &&
301 lvalue.kind !== InstructionKind.Reassign &&
@@ -275,7 +305,10 @@ function visitInstruction(
305 // TODO: only assign Const if the value is never reassigned
306 const kind =
307 range.end === range.start + 1 ? valueKind(instr.value) : DeclKind.Dynamic;
278 - declarations.set(lvalue.place.identifier, { kind, id: instr.id });
308 + context.declare(lvalue.place.identifier, {
309 + kind,
310 + id: lvalue.place.identifier.mutableRange.start,
311 + });
312 }
313 }
314
@@ -286,6 +319,7 @@ function valueKind(value: InstructionValue): DeclKind {
319 case "Primitive": {
320 return DeclKind.Const;
321 }
322 + case "PropertyLoad":
323 case "Identifier":
324 case "ArrayExpression":
325 case "CallExpression":
compiler/forget/src/Utils/DisjointSet.ts
+25 -8
@@ -61,16 +61,33 @@ export default class DisjointSet<T> {
61 if (!this.#entries.has(item)) {
62 return null;
63 }
64 - let current = item;
65 - let parent = this.#entries.get(current)!;
66 - while (current !== parent) {
67 - current = parent;
68 - parent = this.#entries.get(current)!;
64 + const parent = this.#entries.get(item)!;
65 + if (parent === item) {
66 + // this is the root element
67 + return item;
68 }
70 - if (item !== current) {
71 - this.#entries.set(item, current);
69 + // Recurse to find the root (caching all elements along the path to the root)
70 + const root = this.find(parent)!;
71 + // Cache the element itself
72 + this.#entries.set(item, root);
73 + return root;
74 + }
75 +
76 + /**
77 + * Forces the set into canonical form, ie with all items pointing directly to
78 + * their root. Returns true if the set was already in canonical form, false
79 + * otherwise.
80 + */
81 + canonicalize(): boolean {
82 + let isCanonical = true;
83 + for (const item of this.#entries.keys()) {
84 + const parent = this.#entries.get(item)!;
85 + const root = this.find(item);
86 + if (parent !== root) {
87 + isCanonical = false;
88 + }
89 }
73 - return current;
90 + return isCanonical;
91 }
92
93 /**
compiler/forget/src/Utils/logger.ts
+6 -1
@@ -5,8 +5,9 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import { HIR, HIRFunction } from "../HIR/HIR";
8 +import { HIR, HIRFunction, ReactiveFunction } from "../HIR/HIR";
9 import printHIR, { printFunction } from "../HIR/PrintHIR";
10 +import { printReactiveFunction } from "../ReactiveScopes";
11
12 let ENABLED: boolean = false;
13
@@ -22,6 +23,10 @@ export function logHIRFunction(step: string, fn: HIRFunction): void {
23 log(() => `${step}:\n${printFunction(fn)}`);
24 }
25
26 +export function logReactiveFunction(step: string, fn: ReactiveFunction): void {
27 + log(() => `${step}:\n${printReactiveFunction(fn)}`);
28 +}
29 +
30 export function log(fn: () => string) {
31 if (ENABLED) {
32 const message = fn();
compiler/forget/src/__tests__/fixtures/hir/_bug_invalid-scope.expect.md new
+28
@@ -0,0 +1,28 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function g(a) {
6 + a.b.c = a.b.c + 1;
7 + a.b.c *= 2;
8 +}
9 +
10 +```
11 +
12 +## Code
13 +
14 +```javascript
15 +function g(a) {
16 + const $ = React.useMemoCache();
17 + let a;
18 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
19 + a.c.b = a.b.c + 1;
20 + a.c.b = a.b.c * 2;
21 + $[0] = a;
22 + } else {
23 + a = $[0];
24 + }
25 +}
26 +
27 +```
28 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/_bug_invalid-scope.js new
+4
@@ -0,0 +1,4 @@
1 +function g(a) {
2 + a.b.c = a.b.c + 1;
3 + a.b.c *= 2;
4 +}
compiler/forget/src/__tests__/fixtures/hir/assignment-variations-complex-lvalue.expect.md new
+48
@@ -0,0 +1,48 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function g() {
6 + const x = { y: { z: 1 } };
7 + x.y.z = x.y.z + 1;
8 + x.y.z *= 2;
9 + return x;
10 +}
11 +
12 +```
13 +
14 +## Code
15 +
16 +```javascript
17 +function g() {
18 + const $ = React.useMemoCache();
19 + let t0;
20 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
21 + t0 = {
22 + z: 1,
23 + };
24 + $[0] = t0;
25 + } else {
26 + t0 = $[0];
27 + }
28 +
29 + const c_1 = $[1] !== t0;
30 + let x;
31 +
32 + if (c_1) {
33 + x = {
34 + y: t0,
35 + };
36 + x.z.y = x.y.z + 1;
37 + x.z.y = x.y.z * 2;
38 + $[1] = t0;
39 + $[2] = x;
40 + } else {
41 + x = $[2];
42 + }
43 +
44 + return x;
45 +}
46 +
47 +```
48 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/assignment-variations-complex-lvalue.js new
+6
@@ -0,0 +1,6 @@
1 +function g() {
2 + const x = { y: { z: 1 } };
3 + x.y.z = x.y.z + 1;
4 + x.y.z *= 2;
5 + return x;
6 +}
compiler/forget/src/__tests__/fixtures/hir/assignment-variations.expect.md
-14
@@ -9,11 +9,6 @@ function f() {
9 x >>>= 1;
10 }
11
12 -function g(a) {
13 - a.b.c = a.b.c + 1;
14 - a.b.c *= 2;
15 -}
16 -
12 ```
13
14 ## Code
@@ -27,13 +22,4 @@ function f() {
22 }
23
24 ```
30 -## Code
31 -
32 -```javascript
33 -function g(a) {
34 - a.c.b = a.b.c + 1;
35 - a.c.b = a.b.c * 2;
36 -}
37 -
38 -```
25
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/assignment-variations.js
-5
@@ -4,8 +4,3 @@ function f() {
4 x += 1;
5 x >>>= 1;
6 }
7 -
8 -function g(a) {
9 - a.b.c = a.b.c + 1;
10 - a.b.c *= 2;
11 -}
compiler/forget/src/__tests__/fixtures/hir/component.expect.md
+2 -2
@@ -40,7 +40,7 @@ function Component(props) {
40 const maxItems = props.maxItems;
41 const c_0 = $[0] !== maxItems;
42 const c_1 = $[1] !== items.length;
43 - const c_2 = $[2] !== items;
43 + const c_2 = $[2] !== items.at;
44 let renderedItems;
45 if (c_0 || c_1 || c_2) {
46 renderedItems = [];
@@ -77,7 +77,7 @@ function Component(props) {
77
78 $[0] = maxItems;
79 $[1] = items.length;
80 - $[2] = items;
80 + $[2] = items.at;
81 $[3] = renderedItems;
82 } else {
83 renderedItems = $[3];
compiler/forget/src/__tests__/fixtures/hir/issue933-disjoint-set-infinite-loop.expect.md
+9 -37
@@ -13,51 +13,23 @@ function MyApp(props) {
13
14 ```
15
16 -## HIR
17 -
18 -```
19 -bb0:
20 - [1] Const mutate y$7_@0[1:5] = Call mutate makeObj$2:TFunction()
21 - [2] Const mutate tmp$8_@0[1:5] = read y$7_@0.a
22 - [3] Const mutate tmp2$9_@0[1:5] = read tmp$8_@0.b
23 - [4] Call mutate y$7_@0.push(mutate tmp2$9_@0)
24 - [5] Return freeze y$7_@0
25 -```
26 -
27 -## Reactive Scopes
28 -
29 -```
30 -function MyApp(
31 - props,
32 -) {
33 - scope @0 [1:5] deps=[] out=[y$7_@0] {
34 - [1] Const mutate y$7_@0[1:5] = Call mutate makeObj$2:TFunction()
35 - [2] Const mutate tmp$8_@0[1:5] = read y$7_@0.a
36 - [3] Const mutate tmp2$9_@0[1:5] = read tmp$8_@0.b
37 - [4] Call mutate y$7_@0.push(mutate tmp2$9_@0)
38 - }
39 - return freeze y$7_@0
40 -}
41 -
42 -```
43 -
16 ## Code
17
18 ```javascript
47 -function MyApp$0(props$6) {
19 +function MyApp(props) {
20 const $ = React.useMemoCache();
49 - let y$7;
21 + let y;
22 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
51 - y$7 = makeObj$2();
52 - const tmp$8 = y$7.a;
53 - const tmp2$9 = tmp$8.b;
54 - y$7.push(tmp2$9);
55 - $[0] = y$7;
23 + y = makeObj();
24 + const tmp = y.a;
25 + const tmp2 = tmp.b;
26 + y.push(tmp2);
27 + $[0] = y;
28 } else {
57 - y$7 = $[0];
29 + y = $[0];
30 }
31
60 - return y$7;
32 + return y;
33 }
34
35 ```
compiler/forget/src/__tests__/fixtures/hir/simple-scope.expect.md new
+31
@@ -0,0 +1,31 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function foo(a) {
6 + const x = [a.b];
7 + return x;
8 +}
9 +
10 +```
11 +
12 +## Code
13 +
14 +```javascript
15 +function foo(a) {
16 + const $ = React.useMemoCache();
17 + const c_0 = $[0] !== a.b;
18 + let x;
19 + if (c_0) {
20 + x = [a.b];
21 + $[0] = a.b;
22 + $[1] = x;
23 + } else {
24 + x = $[1];
25 + }
26 +
27 + return x;
28 +}
29 +
30 +```
31 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/simple-scope.js new
+4
@@ -0,0 +1,4 @@
1 +function foo(a) {
2 + const x = [a.b];
3 + return x;
4 +}
compiler/forget/src/__tests__/fixtures/hir/while-property.expect.md new
+41
@@ -0,0 +1,41 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function foo(a, b) {
6 + let x = 0;
7 + while (a.b.c) {
8 + x += b;
9 + }
10 + return x;
11 +}
12 +
13 +```
14 +
15 +## Code
16 +
17 +```javascript
18 +function foo(a, b) {
19 + const $ = React.useMemoCache();
20 + const c_0 = $[0] !== a.b.c;
21 + const c_1 = $[1] !== b;
22 + let x;
23 + if (c_0 || c_1) {
24 + x = 0;
25 +
26 + while (a.b.c) {
27 + x = x + b;
28 + }
29 +
30 + $[0] = a.b.c;
31 + $[1] = b;
32 + $[2] = x;
33 + } else {
34 + x = $[2];
35 + }
36 +
37 + return x;
38 +}
39 +
40 +```
41 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/while-property.js new
+7
@@ -0,0 +1,7 @@
1 +function foo(a, b) {
2 + let x = 0;
3 + while (a.b.c) {
4 + x += b;
5 + }
6 + return x;
7 +}