@samitouri / QOS-React-2 / commits / 9be2efdc57

🌲 Create scopes for primitive operations

In normal React certain operations don't allocate new objects (property loads, binary expressions, etc) and therefore don't need a reactive scope in Forget. For example, property loads only extract part of an existing value and don't allocate something new, while binary expressions are known to produce primitive values that don't allocate. We rely on the fact that whenever their inputs change we will re-run the component/hook and propagate the result forward. For Forest, the only way to propagate data is via reactive scopes: the component code is equivalent to a "setup" function. This PR updates some of our passes to ensure that we create (and don't prune) scopes for these types of operations. I started with a conservative set for now.

Joe Savona committed Dec 11, 2023 at 11:34 UTC 9be2efdc57d84f8bb202c8e4c290ed76afbeb82a
3 files changed +23 -27
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts
+1 -5
@@ -279,11 +279,7 @@ function* runWithEnvironment(
279 value: reactiveFunction,
280 });
281
282 - let memoizeJsxElements = env.config.memoizeJsxElements;
283 - if (env.config.enableForest) {
284 - memoizeJsxElements = false;
285 - }
286 - pruneNonEscapingScopes(reactiveFunction, { memoizeJsxElements });
282 + pruneNonEscapingScopes(reactiveFunction);
283 yield log({
284 kind: "reactive",
285 name: "PruneNonEscapingDependencies",
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/InferReactiveScopeVariables.ts
+5 -3
@@ -234,16 +234,13 @@ function mayAllocate(env: Environment, instruction: Instruction): boolean {
234 case "StoreLocal":
235 case "LoadGlobal":
236 case "TypeCastExpression":
237 - case "BinaryExpression":
237 case "LoadLocal":
238 case "LoadContext":
239 case "StoreContext":
241 - case "PropertyLoad":
240 case "PropertyDelete":
241 case "ComputedLoad":
242 case "ComputedDelete":
243 case "JSXText":
246 - case "UnaryExpression":
244 case "TemplateLiteral":
245 case "Primitive":
246 case "NextIterableOf":
@@ -251,6 +248,11 @@ function mayAllocate(env: Environment, instruction: Instruction): boolean {
248 case "Debugger": {
249 return false;
250 }
251 + case "UnaryExpression":
252 + case "BinaryExpression":
253 + case "PropertyLoad": {
254 + return env.config.enableForest;
255 + }
256 case "CallExpression":
257 case "MethodCall": {
258 return instruction.lvalue.identifier.type.kind !== "Primitive";
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PruneNonEscapingScopes.ts
+17 -19
@@ -107,10 +107,7 @@ import {
107 * conditional node with an aliased dep promotes to aliased).
108 * 4. Finally we prune scopes whose outputs weren't marked.
109 */
110 -export function pruneNonEscapingScopes(
111 - fn: ReactiveFunction,
112 - options: MemoizationOptions
113 -): void {
110 +export function pruneNonEscapingScopes(fn: ReactiveFunction): void {
111 /*
112 * First build up a map of which instructions are involved in creating which values,
113 * and which values are returned.
@@ -123,11 +120,7 @@ export function pruneNonEscapingScopes(
120 state.declare(param.place.identifier.id);
121 }
122 }
126 - visitReactiveFunction(
127 - fn,
128 - new CollectDependenciesVisitor(fn.env, options),
129 - state
130 - );
123 + visitReactiveFunction(fn, new CollectDependenciesVisitor(fn.env), state);
124
125 // log(() => prettyFormat(state));
126
@@ -147,6 +140,7 @@ export function pruneNonEscapingScopes(
140
141 export type MemoizationOptions = {
142 memoizeJsxElements: boolean;
143 + forceMemoizePrimitives: boolean;
144 };
145
146 // Describes how to determine whether a value should be memoized, relative to dependees and dependencies
@@ -468,12 +462,12 @@ function computeMemoizationInputs(
462 case "JSXText":
463 case "BinaryExpression":
464 case "UnaryExpression": {
465 + const level = options.forceMemoizePrimitives
466 + ? MemoizationLevel.Memoized
467 + : MemoizationLevel.Never;
468 return {
469 // All of these instructions return a primitive value and never need to be memoized
473 - lvalues:
474 - lvalue !== null
475 - ? [{ place: lvalue, level: MemoizationLevel.Never }]
476 - : [],
470 + lvalues: lvalue !== null ? [{ place: lvalue, level }] : [],
471 rvalues: [],
472 };
473 }
@@ -589,12 +583,12 @@ function computeMemoizationInputs(
583 }
584 case "ComputedLoad":
585 case "PropertyLoad": {
586 + const level = options.forceMemoizePrimitives
587 + ? MemoizationLevel.Memoized
588 + : MemoizationLevel.Conditional;
589 return {
590 // Indirection for the inner value, memoized if the value is
594 - lvalues:
595 - lvalue !== null
596 - ? [{ place: lvalue, level: MemoizationLevel.Conditional }]
597 - : [],
591 + lvalues: lvalue !== null ? [{ place: lvalue, level }] : [],
592 /*
593 * Only the object is aliased to the result, and the result only needs to be
594 * memoized if the object is
@@ -768,10 +762,14 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
762 env: Environment;
763 options: MemoizationOptions;
764
771 - constructor(env: Environment, options: MemoizationOptions) {
765 + constructor(env: Environment) {
766 super();
767 this.env = env;
774 - this.options = options;
768 + this.options = {
769 + memoizeJsxElements:
770 + this.env.config.memoizeJsxElements && !this.env.config.enableForest,
771 + forceMemoizePrimitives: this.env.config.enableForest,
772 + };
773 }
774
775 override visitInstruction(