@samitouri / QOS-React-2 / commits / 314a5cfca5

[hir] Add Load/StoreContext (1/n)

--- This PR adds LoadContext and StoreContext to handle reading and writing to context variables. A context variable is any variable that is declared within a Forget-compiled function and reassigned within a closure. Conceptually, we want to treat these variables as attributes of a `EnvironmentContext` variable (as most javascript VMs do). - context variables currently do not participate in type inference (i.e. we do not produce type equations for loads from context variables). In the future, we can try typing this as `Phi(assignment1Type, assignment2Type, ...)`. - context variables are always treated as `Effect.Mutable`. - context variables do not participate in SSA, or certain optimizing passes (e.g. dead code elimination, constant propagation, etc). There is some still follow ups: - From my understanding, we should introduce a `DeclareContext` instruction. - currently, declaring a context variable (without initializing it) is broken. This is because the declaration lowers to `DeclareLocal`, which assumes it is storing to a SSA-fied identifier. ```js let x; x = 4; () => { x = {}; }; ``` - DeclareContext will also make some initialization logic easier. In this PR, I added some hack-y code to handle initializing effects / mutable ranges / other inference state for the first StoreContext. - Handle or bail on stores to context variables through destructuring assignment - ~~Next PR:~~ - ~~Change closures to track reassigned identifiers (to extend mutable range of primitives)~~

Mofei Zhang committed May 12, 2023 at 12:17 UTC 314a5cfca5f5c26b23b648c59ed7df717016f8a3
28 files changed +549 -57
compiler/forget/src/CompilerPipeline.ts
+3 -1
@@ -18,6 +18,7 @@ import {
18 validateUnconditionalHooks,
19 } from "./HIR";
20 import { Environment, EnvironmentConfig } from "./HIR/Environment";
21 +import { findContextIdentifiers } from "./HIR/FindContextIdentifiers";
22 import {
23 analyseFunctions,
24 dropMemoCalls,
@@ -60,7 +61,8 @@ export function* run(
61 func: NodePath<t.FunctionDeclaration>,
62 config?: EnvironmentConfig | null
63 ): Generator<CompilerPipelineValue, t.FunctionDeclaration> {
63 - const env = new Environment(config ?? null);
64 + const contextIdentifiers = findContextIdentifiers(func);
65 + const env = new Environment(config ?? null, contextIdentifiers);
66 const hir = lower(func, env).unwrap();
67 yield log({ kind: "hir", name: "HIR", value: hir });
68
compiler/forget/src/HIR/BuildHIR.ts
+21 -4
@@ -947,7 +947,7 @@ function lowerExpression(
947 const expr = exprPath as NodePath<t.Identifier>;
948 const place = lowerIdentifier(builder, expr);
949 return {
950 - kind: "LoadLocal",
950 + kind: getLoadKind(builder, expr),
951 place,
952 loc: exprLoc,
953 };
@@ -1367,7 +1367,7 @@ function lowerExpression(
1367 loc: exprLoc,
1368 });
1369 lowerValueToTemporary(builder, {
1370 - kind: "StoreLocal",
1370 + kind: getStoreKind(builder, leftExpr),
1371 lvalue: {
1372 place: { ...identifier },
1373 kind: InstructionKind.Reassign,
@@ -1701,7 +1701,7 @@ function lowerExpression(
1701 loc: exprLoc,
1702 });
1703 lowerValueToTemporary(builder, {
1704 - kind: "StoreLocal",
1704 + kind: getStoreKind(builder, argument),
1705 lvalue: { place: { ...identifier }, kind: InstructionKind.Reassign },
1706 value: { ...temp },
1707 loc: exprLoc,
@@ -2404,6 +2404,22 @@ function buildTemporaryPlace(builder: HIRBuilder, loc: SourceLocation): Place {
2404 return place;
2405 }
2406
2407 +function getStoreKind(
2408 + builder: HIRBuilder,
2409 + identifier: NodePath<t.Identifier>
2410 +): "StoreLocal" | "StoreContext" {
2411 + const isContext = builder.isContextIdentifier(identifier);
2412 + return isContext ? "StoreContext" : "StoreLocal";
2413 +}
2414 +
2415 +function getLoadKind(
2416 + builder: HIRBuilder,
2417 + identifier: NodePath<t.Identifier>
2418 +): "LoadLocal" | "LoadContext" {
2419 + const isContext = builder.isContextIdentifier(identifier);
2420 + return isContext ? "LoadContext" : "LoadLocal";
2421 +}
2422 +
2423 function lowerAssignment(
2424 builder: HIRBuilder,
2425 loc: SourceLocation,
@@ -2446,7 +2462,7 @@ function lowerAssignment(
2462 loc: lvalue.node.loc ?? GeneratedSource,
2463 };
2464 const temporary = lowerValueToTemporary(builder, {
2449 - kind: "StoreLocal",
2465 + kind: getStoreKind(builder, lvalue),
2466 lvalue: { place: { ...place }, kind },
2467 value,
2468 loc,
@@ -2501,6 +2517,7 @@ function lowerAssignment(
2517 }
2518 }
2519 case "ArrayPattern": {
2520 + // TODO
2521 const lvalue = lvaluePath as NodePath<t.ArrayPattern>;
2522 const elements = lvalue.get("elements");
2523 const items: ArrayPattern["items"] = [];
compiler/forget/src/HIR/Environment.ts
+10 -1
@@ -5,6 +5,7 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 +import * as t from "@babel/types";
9 import invariant from "invariant";
10 import { log } from "../Utils/logger";
11 import {
@@ -47,8 +48,12 @@ export class Environment {
48 #nextIdentifer: number = 0;
49 #nextBlock: number = 0;
50 validateHooksUsage: boolean;
51 + #contextIdentifiers: Set<t.Identifier>;
52
51 - constructor(config: EnvironmentConfig | null) {
53 + constructor(
54 + config: EnvironmentConfig | null,
55 + contextIdentifiers: Set<t.Identifier>
56 + ) {
57 this.#shapes = DEFAULT_SHAPES;
58
59 if (config?.customHooks) {
@@ -67,6 +72,7 @@ export class Environment {
72 this.#globals = DEFAULT_GLOBALS;
73 }
74 this.validateHooksUsage = config?.validateHooksUsage ?? false;
75 + this.#contextIdentifiers = contextIdentifiers;
76 }
77
78 get nextIdentifierId(): IdentifierId {
@@ -76,6 +82,9 @@ export class Environment {
82 get nextBlockId(): BlockId {
83 return makeBlockId(this.#nextBlock++);
84 }
85 + isContextIdentifier(node: t.Identifier): boolean {
86 + return this.#contextIdentifiers.has(node);
87 + }
88
89 getGlobalDeclaration(name: string): Global | null {
90 let resolvedGlobal: Global | null = this.#globals.get(name) ?? null;
compiler/forget/src/HIR/FindContextIdentifiers.ts new
+154
@@ -0,0 +1,154 @@
1 +import { NodePath } from "@babel/traverse";
2 +import * as t from "@babel/types";
3 +import { CompilerError } from "../CompilerError";
4 +import { GeneratedSource } from "./HIR";
5 +
6 +type FindContextIdentifierState = {
7 + inLambda: number;
8 + currentLambda: Array<
9 + NodePath<t.FunctionExpression> | NodePath<t.ArrowFunctionExpression>
10 + >;
11 + contextIdentifiers: Set<t.Identifier>;
12 +};
13 +
14 +export function findContextIdentifiers(
15 + func: NodePath<t.Function>
16 +): Set<t.Identifier> {
17 + const state: FindContextIdentifierState = {
18 + inLambda: 0,
19 + currentLambda: [],
20 + contextIdentifiers: new Set(),
21 + };
22 +
23 + func.traverse<FindContextIdentifierState>(
24 + {
25 + FunctionExpression: {
26 + enter(
27 + fn: NodePath<t.FunctionExpression>,
28 + state: FindContextIdentifierState
29 + ): void {
30 + state.currentLambda.push(fn);
31 + },
32 + exit(
33 + fn: NodePath<t.FunctionExpression>,
34 + state: FindContextIdentifierState
35 + ): void {
36 + state.currentLambda.pop();
37 + },
38 + },
39 +
40 + ArrowFunctionExpression: {
41 + enter(
42 + fn: NodePath<t.ArrowFunctionExpression>,
43 + state: FindContextIdentifierState
44 + ): void {
45 + state.currentLambda.push(fn);
46 + },
47 + exit(
48 + fn: NodePath<t.ArrowFunctionExpression>,
49 + state: FindContextIdentifierState
50 + ): void {
51 + state.currentLambda.pop();
52 + },
53 + },
54 + AssignmentExpression(
55 + path: NodePath<t.AssignmentExpression>,
56 + state: FindContextIdentifierState
57 + ): void {
58 + const currentLambda = state.currentLambda.at(-1);
59 + if (currentLambda) {
60 + const left = path.get("left");
61 + handleAssignment(currentLambda, state.contextIdentifiers, left);
62 + }
63 + },
64 + },
65 + state
66 + );
67 + return state.contextIdentifiers;
68 +}
69 +
70 +function handleAssignment(
71 + currentLambda:
72 + | NodePath<t.FunctionExpression>
73 + | NodePath<t.ArrowFunctionExpression>,
74 + contextIdentifiers: Set<t.Identifier>,
75 + lvalPath: NodePath<t.LVal>
76 +): void {
77 + // Find all reassignments to identifiers declared outside of currentLambda
78 + // This closely follows destructuring assignment assumptions and logic in BuildHIR
79 + const lvalNode = lvalPath.node;
80 + switch (lvalNode.type) {
81 + case "Identifier": {
82 + const path = lvalPath as NodePath<t.Identifier>;
83 + const name = path.node.name;
84 + const ownBinding = path.scope.getBinding(name);
85 + const bindingAboveLambdaScope =
86 + currentLambda.scope.parent.getBinding(name);
87 +
88 + if (ownBinding != null && ownBinding === bindingAboveLambdaScope) {
89 + contextIdentifiers.add(ownBinding.identifier);
90 + }
91 + break;
92 + }
93 + case "ArrayPattern": {
94 + const path = lvalPath as NodePath<t.ArrayPattern>;
95 + for (const element of path.get("elements")) {
96 + if (nonNull(element)) {
97 + handleAssignment(currentLambda, contextIdentifiers, element);
98 + }
99 + }
100 + break;
101 + }
102 + case "ObjectPattern": {
103 + const path = lvalPath as NodePath<t.ObjectPattern>;
104 + for (const property of path.get("properties")) {
105 + if (property.isObjectProperty()) {
106 + const valuePath = property.get("value");
107 + if (!valuePath.isLVal()) {
108 + CompilerError.invariant(
109 + `[FindContextIdentifiers] Expected object property value to be an LVal, got: ${valuePath.type}`,
110 + valuePath.node.loc ?? GeneratedSource
111 + );
112 + }
113 + handleAssignment(currentLambda, contextIdentifiers, valuePath);
114 + } else {
115 + if (!property.isRestElement()) {
116 + CompilerError.invariant(
117 + `[FindContextIdentifiers] Invalid assumptions for babel types.`,
118 + property.node.loc ?? GeneratedSource
119 + );
120 + }
121 + handleAssignment(currentLambda, contextIdentifiers, property);
122 + }
123 + }
124 + break;
125 + }
126 + case "AssignmentPattern": {
127 + const path = lvalPath as NodePath<t.AssignmentPattern>;
128 + const left = path.get("left");
129 + handleAssignment(currentLambda, contextIdentifiers, left);
130 + break;
131 + }
132 + case "RestElement": {
133 + const path = lvalPath as NodePath<t.RestElement>;
134 + handleAssignment(currentLambda, contextIdentifiers, path.get("argument"));
135 + break;
136 + }
137 + case "MemberExpression": {
138 + // Interior mutability (not a reassign)
139 + break;
140 + }
141 + default: {
142 + CompilerError.todo(
143 + `[FindContextIdentifiers] Cannot handle Object destructuring assignment target ${lvalNode.type}`,
144 + lvalNode.loc ?? GeneratedSource
145 + );
146 + }
147 + }
148 +}
149 +
150 +function nonNull<T extends NonNullable<t.Node>>(
151 + t: NodePath<T | null>
152 +): t is NodePath<T> {
153 + return t.node != null;
154 +}
compiler/forget/src/HIR/HIR.ts
+11
@@ -552,6 +552,11 @@ export type InstructionValue =
552 place: Place;
553 loc: SourceLocation;
554 }
555 + | {
556 + kind: "LoadContext";
557 + place: Place;
558 + loc: SourceLocation;
559 + }
560 | {
561 kind: "DeclareLocal";
562 lvalue: LValue;
@@ -563,6 +568,12 @@ export type InstructionValue =
568 value: Place;
569 loc: SourceLocation;
570 }
571 + | {
572 + kind: "StoreContext";
573 + lvalue: LValue;
574 + value: Place;
575 + loc: SourceLocation;
576 + }
577 | {
578 kind: "Destructure";
579 lvalue: LValuePattern;
compiler/forget/src/HIR/HIRBuilder.ts
+44 -25
@@ -5,7 +5,7 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import { NodePath } from "@babel/traverse";
8 +import { Binding, NodePath } from "@babel/traverse";
9 import * as t from "@babel/types";
10 import invariant from "invariant";
11 import { CompilerError } from "../CompilerError";
@@ -22,10 +22,10 @@ import {
22 Identifier,
23 IdentifierId,
24 Instruction,
25 + Terminal,
26 makeBlockId,
27 makeInstructionId,
28 makeType,
28 - Terminal,
29 } from "./HIR";
30 import { printInstruction } from "./PrintHIR";
31 import {
@@ -164,6 +164,35 @@ export default class HIRBuilder {
164 }
165 }
166
167 + #resolveBabelBinding(
168 + path: NodePath<t.Identifier | t.JSXIdentifier>
169 + ): Binding | null {
170 + const originalName = path.node.name;
171 + const binding = path.scope.getBinding(originalName);
172 + if (binding == null) {
173 + return null;
174 + }
175 + // If the binding is from the parent function's outer scope, then
176 + // we treat it equivalently to a global.
177 + //
178 + // TODO: remove the exception that resolves references to the
179 + // parent function itself. We don't need to support self-recursion,
180 + // so we can treat such references as globals.
181 + const outerBinding =
182 + this.parentFunction.scope.parent.getBinding(originalName);
183 + if (binding === outerBinding) {
184 + const func = this.parentFunction;
185 + const isParentFunctionReference =
186 + func.isFunctionDeclaration() &&
187 + func.get("id").node != null &&
188 + func.get("id").node!.name === originalName;
189 + if (!isParentFunctionReference) {
190 + return null;
191 + }
192 + }
193 + return binding;
194 + }
195 +
196 /**
197 * Maps an Identifier (or JSX identifier) Babel node to an internal `Identifier`
198 * which represents the variable being referenced, according to the JS scoping rules.
@@ -198,36 +227,26 @@ export default class HIRBuilder {
227 path: NodePath<t.Identifier | t.JSXIdentifier>
228 ): Identifier | null {
229 const originalName = path.node.name;
201 - const binding = path.scope.getBinding(originalName);
202 - if (binding == null) {
230 + const babelBinding = this.#resolveBabelBinding(path);
231 + if (babelBinding == null) {
232 return null;
233 }
205 - // If the binding is from the parent function's outer scope, then
206 - // we treat it equivalently to a global.
207 - //
208 - // TODO: remove the exception that resolves references to the
209 - // parent function itself. We don't need to support self-recursion,
210 - // so we can treat such references as globals.
211 - const outerBinding =
212 - this.parentFunction.scope.parent.getBinding(originalName);
213 - if (binding === outerBinding) {
214 - const func = this.parentFunction;
215 - const isParentFunctionReference =
216 - func.isFunctionDeclaration() &&
217 - func.get("id").node != null &&
218 - func.get("id").node!.name === originalName;
219 - if (!isParentFunctionReference) {
220 - return null;
221 - }
222 - }
223 -
224 - const resolvedBinding = this.resolveBinding(binding.identifier);
234 + const resolvedBinding = this.resolveBinding(babelBinding.identifier);
235 if (resolvedBinding.name && resolvedBinding.name !== originalName) {
226 - binding.scope.rename(originalName, resolvedBinding.name);
236 + babelBinding.scope.rename(originalName, resolvedBinding.name);
237 }
238 return resolvedBinding;
239 }
240
241 + isContextIdentifier(path: NodePath<t.Identifier>): boolean {
242 + const binding = this.#resolveBabelBinding(path);
243 + if (binding) {
244 + return this.#env.isContextIdentifier(binding.identifier);
245 + } else {
246 + return false;
247 + }
248 + }
249 +
250 resolveBinding(node: t.Identifier): Identifier {
251 const originalName = node.name;
252 let name = originalName;
compiler/forget/src/HIR/PrintHIR.ts
+11 -1
@@ -117,7 +117,7 @@ export function printInstruction(instr: ReactiveInstruction): string {
117 }
118 }
119
120 -function printPhi(phi: Phi): string {
120 +export function printPhi(phi: Phi): string {
121 const items = [];
122 items.push(printIdentifier(phi.id));
123 items.push(printMutableRange(phi.id));
@@ -360,6 +360,16 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
360 )} = ${printPlace(instrValue.value)}`;
361 break;
362 }
363 + case "LoadContext": {
364 + value = `LoadContext ${printPlace(instrValue.place)}`;
365 + break;
366 + }
367 + case "StoreContext": {
368 + value = `StoreContext ${instrValue.lvalue.kind} ${printPlace(
369 + instrValue.lvalue.place
370 + )} = ${printPlace(instrValue.value)}`;
371 + break;
372 + }
373 case "Destructure": {
374 value = `Destructure ${instrValue.lvalue.kind} ${printPattern(
375 instrValue.lvalue.pattern
compiler/forget/src/HIR/visitors.ts
+14 -2
@@ -64,7 +64,8 @@ export function* eachInstructionValueOperand(
64 case "DeclareLocal": {
65 break;
66 }
67 - case "LoadLocal": {
67 + case "LoadLocal":
68 + case "LoadContext": {
69 yield instrValue.place;
70 break;
71 }
@@ -72,6 +73,11 @@ export function* eachInstructionValueOperand(
73 yield instrValue.value;
74 break;
75 }
76 + case "StoreContext": {
77 + yield instrValue.lvalue.place;
78 + yield instrValue.value;
79 + break;
80 + }
81 case "Destructure": {
82 yield instrValue.value;
83 break;
@@ -348,7 +354,8 @@ export function mapInstructionOperands(
354 case "DeclareLocal": {
355 break;
356 }
351 - case "LoadLocal": {
357 + case "LoadLocal":
358 + case "LoadContext": {
359 instrValue.place = fn(instrValue.place);
360 break;
361 }
@@ -356,6 +363,11 @@ export function mapInstructionOperands(
363 instrValue.value = fn(instrValue.value);
364 break;
365 }
366 + case "StoreContext": {
367 + instrValue.lvalue.place = fn(instrValue.lvalue.place);
368 + instrValue.value = fn(instrValue.value);
369 + break;
370 + }
371 case "Destructure": {
372 instrValue.value = fn(instrValue.value);
373 break;
compiler/forget/src/Inference/AnalyseFunctions.ts
+2 -1
@@ -75,7 +75,8 @@ export default function analyseFunctions(func: HIRFunction): void {
75 state.declareProperty(instr.lvalue, instr.value.object, "");
76 break;
77 }
78 - case "LoadLocal": {
78 + case "LoadLocal":
79 + case "LoadContext": {
80 if (instr.lvalue.identifier.name === null) {
81 state.declareTemporary(instr.lvalue, instr.value.place);
82 }
compiler/forget/src/Inference/InferAlias.ts
+4 -2
@@ -34,14 +34,16 @@ function inferInstr(
34 const { lvalue, value: instrValue } = instr;
35 let alias: Place | null = null;
36 switch (instrValue.kind) {
37 - case "LoadLocal": {
37 + case "LoadLocal":
38 + case "LoadContext": {
39 if (isPrimitiveType(instrValue.place.identifier)) {
40 return;
41 }
42 alias = instrValue.place;
43 break;
44 }
44 - case "StoreLocal": {
45 + case "StoreLocal":
46 + case "StoreContext": {
47 alias = instrValue.value;
48 break;
49 }
compiler/forget/src/Inference/InferMutableLifetimes.ts
+10
@@ -118,6 +118,16 @@ export function inferMutableLifetimes(
118 }
119
120 for (const instr of block.instructions) {
121 + if (instr.value.kind === "StoreContext") {
122 + const id = instr.value.lvalue.place.identifier;
123 + // Context variables do not participate in SSA and are not generally considered
124 + // lvalues (). This hack tries to initialize a mutable range the first time we
125 + // visit an context variable assignment.
126 + if (id.mutableRange.start === 0 && id.mutableRange.end === 0) {
127 + id.mutableRange.start = instr.id;
128 + id.mutableRange.end = makeInstructionId(instr.id + 1);
129 + }
130 + }
131 for (const operand of eachInstructionLValue(instr)) {
132 const lvalueId = operand.identifier;
133
compiler/forget/src/Inference/InferReferenceEffects.ts
+43
@@ -282,6 +282,12 @@ class InferenceState {
282 #referenceImpl(place: Place, effectKind: Effect, shouldError: boolean): void {
283 const values = this.#variables.get(place.identifier.id);
284 if (values === undefined) {
285 + if (effectKind === Effect.Store) {
286 + CompilerError.invariant(
287 + "[InferReferenceEffects] Unhandled store reference effect",
288 + place.loc
289 + );
290 + }
291 place.effect = effectKind === Effect.Mutate ? Effect.Mutate : Effect.Read;
292 return;
293 }
@@ -849,6 +855,19 @@ function inferBlock(
855 state.alias(lvalue, instrValue.place);
856 continue;
857 }
858 + case "LoadContext": {
859 + state.reference(instrValue.place, Effect.Capture);
860 + const lvalue = instr.lvalue;
861 + lvalue.effect = Effect.Mutate;
862 + const valueKind = state.kind(instrValue.place);
863 + invariant(
864 + valueKind === ValueKind.Mutable || valueKind === ValueKind.Context,
865 + "[InferReferenceEffects] Context variables are always mutable."
866 + );
867 + state.initialize(instrValue, valueKind);
868 + state.define(lvalue, instrValue);
869 + continue;
870 + }
871 case "DeclareLocal": {
872 const value: InstructionValue = {
873 kind: "Primitive",
@@ -876,6 +895,30 @@ function inferBlock(
895 state.reference(instrValue.lvalue.place, Effect.Store);
896 continue;
897 }
898 + case "StoreContext": {
899 + state.reference(instrValue.value, Effect.Mutate);
900 + state.reference(instrValue.lvalue.place, Effect.Mutate);
901 +
902 + const lvalue = instr.lvalue;
903 + state.alias(lvalue, instrValue.value);
904 + // this logic is really awkward
905 + // Essentially, we want to say that
906 + // 1. instr.lvalue (the value produced by the instruction itself) has a
907 + // ValueKind of the rhs.
908 + // - this is for chained assignment
909 + // 2. instr.value.lvalue (the store location) has a ValueKind of Mutable
910 +
911 + // As an alternative, we could insert a CreateContextVariable instruction
912 + // before the initial StoreContext
913 + const storeLValue = instrValue.lvalue.place;
914 + if (!state.isDefined(storeLValue)) {
915 + const instrCopy = { ...instrValue };
916 + state.initialize(instrCopy, ValueKind.Mutable);
917 + state.define(storeLValue, instrCopy);
918 + }
919 + lvalue.effect = Effect.Store;
920 + continue;
921 + }
922 case "Destructure": {
923 let effect: Effect = Effect.Capture;
924 for (const place of eachPatternOperand(instrValue.lvalue.pattern)) {
compiler/forget/src/Optimization/DeadCodeElimination.ts
+4
@@ -222,6 +222,10 @@ function pruneableValue(value: InstructionValue, state: State): boolean {
222 // another StoreLocal or Destructure instruction, but conceptually we can't prune
223 return false;
224 }
225 + case "LoadContext":
226 + case "StoreContext": {
227 + return false;
228 + }
229 case "RegExpLiteral":
230 case "LoadGlobal":
231 case "ArrayExpression":
compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+9 -3
@@ -441,13 +441,17 @@ function codegenInstructionNullable(
441 ): t.Statement | null {
442 if (
443 instr.value.kind === "StoreLocal" ||
444 + instr.value.kind === "StoreContext" ||
445 instr.value.kind === "Destructure" ||
446 instr.value.kind === "DeclareLocal"
447 ) {
448 let kind: InstructionKind = instr.value.lvalue.kind;
449 let lvalue;
450 let value: t.Expression | null;
450 - if (instr.value.kind === "StoreLocal") {
451 + if (
452 + instr.value.kind === "StoreLocal" ||
453 + instr.value.kind === "StoreContext"
454 + ) {
455 kind = cx.hasDeclared(instr.value.lvalue.place.identifier)
456 ? InstructionKind.Reassign
457 : kind;
@@ -899,7 +903,8 @@ function codegenInstructionValue(
903 );
904 break;
905 }
902 - case "LoadLocal": {
906 + case "LoadLocal":
907 + case "LoadContext": {
908 value = codegenPlace(cx, instrValue.place);
909 break;
910 }
@@ -1013,7 +1018,8 @@ function codegenInstructionValue(
1018 case "Debugger":
1019 case "DeclareLocal":
1020 case "Destructure":
1016 - case "StoreLocal": {
1021 + case "StoreLocal":
1022 + case "StoreContext": {
1023 CompilerError.invariant(
1024 `Unexpected ${instrValue.kind} in codegenInstructionValue`,
1025 instrValue.loc
compiler/forget/src/ReactiveScopes/InferReactiveScopeVariables.ts
+6 -1
@@ -106,7 +106,10 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
106 if (range.end > range.start + 1 || mayAllocate(instr.value)) {
107 operands.push(instr.lvalue!.identifier);
108 }
109 - if (instr.value.kind === "StoreLocal") {
109 + if (
110 + instr.value.kind === "StoreLocal" ||
111 + instr.value.kind === "StoreContext"
112 + ) {
113 if (
114 instr.value.lvalue.place.identifier.mutableRange.end >
115 instr.value.lvalue.place.identifier.mutableRange.start + 1
@@ -226,6 +229,8 @@ function mayAllocate(value: InstructionValue): boolean {
229 case "TypeCastExpression":
230 case "BinaryExpression":
231 case "LoadLocal":
232 + case "LoadContext":
233 + case "StoreContext":
234 case "PropertyLoad":
235 case "PropertyDelete":
236 case "ComputedLoad":
compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts
+6 -2
@@ -91,6 +91,7 @@ class FindPromotedTemporaries extends ReactiveFunctionVisitor<TemporariesUsedOut
91 }
92 switch (instruction.value.kind) {
93 case "LoadLocal":
94 + case "LoadContext":
95 case "PropertyLoad": {
96 state.declarations.set(instruction.lvalue.identifier.id, scope);
97 break;
@@ -512,7 +513,10 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
513 value: ReactiveValue,
514 lvalue: Place | null
515 ): void {
515 - if (value.kind === "LoadLocal" && lvalue !== null) {
516 + if (
517 + (value.kind === "LoadLocal" || value.kind === "LoadContext") &&
518 + lvalue !== null
519 + ) {
520 if (
521 value.place.identifier.name !== null &&
522 lvalue.identifier.name === null &&
@@ -528,7 +532,7 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
532 } else {
533 context.visitProperty(value.object, value.property);
534 }
531 - } else if (value.kind === "StoreLocal") {
535 + } else if (value.kind === "StoreLocal" || value.kind === "StoreContext") {
536 context.visitOperand(value.value);
537 if (value.lvalue.kind === InstructionKind.Reassign) {
538 context.visitReassignment(value.lvalue.place);
compiler/forget/src/ReactiveScopes/PruneNonEscapingScopes.ts
+24
@@ -461,6 +461,16 @@ function computeMemoizationInputs(
461 rvalues: [value.place],
462 };
463 }
464 + case "LoadContext": {
465 + return {
466 + // Should never be pruned
467 + lvalues:
468 + lvalue !== null
469 + ? [{ place: lvalue, level: MemoizationLevel.Conditional }]
470 + : [],
471 + rvalues: [value.place],
472 + };
473 + }
474 case "DeclareLocal": {
475 const lvalues = [
476 { place: value.lvalue.place, level: MemoizationLevel.Unmemoized },
@@ -486,6 +496,20 @@ function computeMemoizationInputs(
496 rvalues: [value.value],
497 };
498 }
499 + case "StoreContext": {
500 + // Should never be pruned
501 + const lvalues = [
502 + { place: value.lvalue.place, level: MemoizationLevel.Memoized },
503 + ];
504 + if (lvalue !== null) {
505 + lvalues.push({ place: lvalue, level: MemoizationLevel.Conditional });
506 + }
507 +
508 + return {
509 + lvalues,
510 + rvalues: [value.value],
511 + };
512 + }
513 case "Destructure": {
514 // Indirection for the inner value, memoized if the value is
515 const lvalues = [];
compiler/forget/src/TypeInference/InferTypes.ts
+8
@@ -124,6 +124,14 @@ function* generateInstructionTypes(
124 break;
125 }
126
127 + // For now, we won't infer types for context variables
128 + case "StoreContext": {
129 + break;
130 + }
131 + case "LoadContext": {
132 + yield equation(left, value.place.identifier.type);
133 + break;
134 + }
135 case "StoreLocal": {
136 yield equation(left, value.value.identifier.type);
137 yield equation(
compiler/forget/src/__tests__/fixtures/compiler/_bug.capturing-reference-changes-type.expect.md
+17 -7
@@ -17,14 +17,24 @@ function component(a) {
17 ## Code
18
19 ```javascript
20 +import { unstable_useMemoCache as useMemoCache } from "react";
21 function component(a) {
21 - const x = { a };
22 -
23 - (function () {
24 - y = x;
25 - })();
26 - mutate(1);
27 - return 1;
22 + const $ = useMemoCache(2);
23 + const c_0 = $[0] !== a;
24 + let y;
25 + if (c_0) {
26 + const x = { a };
27 + y = 1;
28 + (function () {
29 + y = x;
30 + })();
31 + mutate(y);
32 + $[0] = a;
33 + $[1] = y;
34 + } else {
35 + y = $[1];
36 + }
37 + return y;
38 }
39
40 ```
compiler/forget/src/__tests__/fixtures/compiler/_bug.destructure-to-local-global-variables.expect.md new
+35
@@ -0,0 +1,35 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + let a;
7 + [a, b] = props.value;
8 +
9 + return [a, b];
10 +}
11 +
12 +```
13 +
14 +## Code
15 +
16 +```javascript
17 +import { unstable_useMemoCache as useMemoCache } from "react";
18 +function Component(props) {
19 + const $ = useMemoCache(2);
20 +
21 + const [a] = props.value;
22 + const c_0 = $[0] !== a;
23 + let t0;
24 + if (c_0) {
25 + t0 = [a, b];
26 + $[0] = a;
27 + $[1] = t0;
28 + } else {
29 + t0 = $[1];
30 + }
31 + return t0;
32 +}
33 +
34 +```
35 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/_bug.destructure-to-local-global-variables.js new
+6
@@ -0,0 +1,6 @@
1 +function Component(props) {
2 + let a;
3 + [a, b] = props.value;
4 +
5 + return [a, b];
6 +}
compiler/forget/src/__tests__/fixtures/compiler/_bug.lambda-reassign-primitive.expect.md
+15 -6
@@ -22,17 +22,26 @@ function Component() {
22 ## Code
23
24 ```javascript
25 -// writing to primitives is not a 'mutate' or 'store' to context references,
25 +import { unstable_useMemoCache as useMemoCache } from "react"; // writing to primitives is not a 'mutate' or 'store' to context references,
26 // under current analysis in AnalyzeFunctions.
27 // <unknown> $23:TFunction = Function @deps[<unknown>
28 // $21:TPrimitive,<unknown> $22:TPrimitive]:
29
30 function Component() {
31 - const fn = function () {
32 - x = x + 1;
33 - };
34 - fn();
35 - return 40;
31 + const $ = useMemoCache(1);
32 + let x;
33 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
34 + x = 40;
35 +
36 + const fn = function () {
37 + x = x + 1;
38 + };
39 + fn();
40 + $[0] = x;
41 + } else {
42 + x = $[0];
43 + }
44 + return x;
45 }
46
47 ```
compiler/forget/src/__tests__/fixtures/compiler/_bug.lambda-reassign-shadowed-primitive.expect.md
+1
@@ -31,6 +31,7 @@ function Component() {
31 }
32 const x = t0;
33
34 + let x_0 = 56;
35 const fn = function () {
36 x_0 = 42;
37 };
compiler/forget/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-3.expect.md
+1 -1
@@ -28,7 +28,7 @@ function bar(a, b) {
28 if (c_0 || c_1) {
29 const x = [a, b];
30 y = {};
31 - const t = {};
31 + let t = {};
32 (function () {
33 y = x[0][1];
34 t = x[1][0];
compiler/forget/src/__tests__/fixtures/compiler/reassign-object-in-context.expect.md new
+37
@@ -0,0 +1,37 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + let x = [];
7 + let foo = () => {
8 + x = {};
9 + };
10 + foo();
11 + return x;
12 +}
13 +
14 +```
15 +
16 +## Code
17 +
18 +```javascript
19 +import { unstable_useMemoCache as useMemoCache } from "react";
20 +function Component(props) {
21 + const $ = useMemoCache(1);
22 + let x;
23 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
24 + x = [];
25 + const foo = () => {
26 + x = {};
27 + };
28 + foo();
29 + $[0] = x;
30 + } else {
31 + x = $[0];
32 + }
33 + return x;
34 +}
35 +
36 +```
37 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/reassign-object-in-context.js new
+8
@@ -0,0 +1,8 @@
1 +function Component(props) {
2 + let x = [];
3 + let foo = () => {
4 + x = {};
5 + };
6 + foo();
7 + return x;
8 +}
compiler/forget/src/__tests__/fixtures/compiler/reassign-primitive-in-context.expect.md new
+37
@@ -0,0 +1,37 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + let x = 5;
7 + let foo = () => {
8 + x = {};
9 + };
10 + foo();
11 + return x;
12 +}
13 +
14 +```
15 +
16 +## Code
17 +
18 +```javascript
19 +import { unstable_useMemoCache as useMemoCache } from "react";
20 +function Component(props) {
21 + const $ = useMemoCache(1);
22 + let x;
23 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
24 + x = 5;
25 + const foo = () => {
26 + x = {};
27 + };
28 + foo();
29 + $[0] = x;
30 + } else {
31 + x = $[0];
32 + }
33 + return x;
34 +}
35 +
36 +```
37 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/reassign-primitive-in-context.js new
+8
@@ -0,0 +1,8 @@
1 +function Component(props) {
2 + let x = 5;
3 + let foo = () => {
4 + x = {};
5 + };
6 + foo();
7 + return x;
8 +}