Codegen comments to explain output
Joe Savona committed
Nov 10, 2023 at 11:45 UTC
f92a058ca5ebde1566d70f85cfd40d7fb5cfc38b
4 files changed
+192
-1
compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts
+8
@@ -257,6 +257,14 @@ const EnvironmentConfigSchema = z.object({
257
* ```
258
*/
259
enableChangeVariableCodegen: z.boolean().default(false),
260
+
261
+ /**
262
+ * Enable emitting comments that explain Forget's output, and which
263
+ * values are being checked and which values produced by each memo block.
264
+ *
265
+ * Intended for use in demo purposes (incl playground)
266
+ */
267
+ enableMemoizationComments: z.boolean().default(false),
268
});
269
270
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+94
-1
@@ -240,9 +240,12 @@ function codegenMemoBlockForReactiveScope(
240
const cacheStoreStatements: Array<t.Statement> = [];
241
const cacheLoadStatements: Array<t.Statement> = [];
242
const changeExpressions: Array<t.Expression> = [];
243
+ const changeExpressionComments: Array<string> = [];
244
+ const outputComments: Array<string> = [];
245
for (const dep of scope.dependencies) {
246
const index = cx.nextCacheIndex;
247
const depValue = codegenDependency(cx, dep);
248
+ changeExpressionComments.push(printDependencyComment(dep));
249
const comparison = t.binaryExpression(
250
"!==",
251
t.memberExpression(t.identifier("$"), t.numericLiteral(index), true),
@@ -285,6 +288,7 @@ function codegenMemoBlockForReactiveScope(
288
});
289
290
const name = convertIdentifier(identifier);
291
+ outputComments.push(name.name);
292
if (!cx.hasDeclared(identifier)) {
293
statements.push(
294
t.variableDeclaration("let", [t.variableDeclarator(name)])
@@ -316,6 +320,7 @@ function codegenMemoBlockForReactiveScope(
320
firstOutputIndex = index;
321
}
322
const name = convertIdentifier(reassignment);
323
+ outputComments.push(name.name);
324
325
cacheStoreStatements.push(
326
t.expressionStatement(
@@ -369,7 +374,60 @@ function codegenMemoBlockForReactiveScope(
374
const computationBlock = codegenBlock(cx, block);
375
computationBlock.body.push(...cacheStoreStatements);
376
const memoBlock = t.blockStatement(cacheLoadStatements);
372
- statements.push(t.ifStatement(testCondition, computationBlock, memoBlock));
377
+ const memoStatement = t.ifStatement(
378
+ testCondition,
379
+ computationBlock,
380
+ memoBlock
381
+ );
382
+ if (cx.env.config.enableMemoizationComments) {
383
+ if (changeExpressionComments.length) {
384
+ t.addComment(
385
+ memoStatement,
386
+ "leading",
387
+ ` check if ${printDelimitedCommentList(
388
+ changeExpressionComments,
389
+ "or"
390
+ )} changed`,
391
+ true
392
+ );
393
+ t.addComment(
394
+ memoStatement,
395
+ "leading",
396
+ ` "useMemo" for ${printDelimitedCommentList(outputComments, "and")}:`,
397
+ true
398
+ );
399
+ } else {
400
+ t.addComment(
401
+ memoStatement,
402
+ "leading",
403
+ " cache value with no dependencies",
404
+ true
405
+ );
406
+ t.addComment(
407
+ memoStatement,
408
+ "leading",
409
+ ` "useMemo" for ${printDelimitedCommentList(outputComments, "and")}:`,
410
+ true
411
+ );
412
+ }
413
+ if (computationBlock.body.length > 0) {
414
+ t.addComment(
415
+ computationBlock.body[0]!,
416
+ "leading",
417
+ ` Inputs changed, recompute`,
418
+ true
419
+ );
420
+ }
421
+ if (memoBlock.body.length > 0) {
422
+ t.addComment(
423
+ memoBlock.body[0]!,
424
+ "leading",
425
+ ` Inputs did not change, use cached value`,
426
+ true
427
+ );
428
+ }
429
+ }
430
+ statements.push(memoStatement);
431
}
432
433
function codegenSignalBlockForReactiveScope(
@@ -789,6 +847,41 @@ function codegenForInit(
847
}
848
}
849
850
+function printDependencyComment(dependency: ReactiveScopeDependency): string {
851
+ const identifier = convertIdentifier(dependency.identifier);
852
+ let name = identifier.name;
853
+ if (dependency.path !== null) {
854
+ for (const path of dependency.path) {
855
+ name += `.${path}`;
856
+ }
857
+ }
858
+ return name;
859
+}
860
+
861
+function printDelimitedCommentList(
862
+ items: Array<string>,
863
+ finalCompletion: string
864
+): string {
865
+ if (items.length === 2) {
866
+ return items.join(` ${finalCompletion} `);
867
+ } else if (items.length <= 1) {
868
+ return items.join("");
869
+ }
870
+
871
+ let output = [];
872
+ for (let i = 0; i < items.length; i++) {
873
+ const item = items[i]!;
874
+ if (i < items.length - 2) {
875
+ output.push(`${item}, `);
876
+ } else if (i === items.length - 2) {
877
+ output.push(`${item}, ${finalCompletion} `);
878
+ } else {
879
+ output.push(item);
880
+ }
881
+ }
882
+ return output.join("");
883
+}
884
+
885
function codegenDependency(
886
cx: Context,
887
dependency: ReactiveScopeDependency
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/memoization-comments.expect.md
new
+76
@@ -0,0 +1,76 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableMemoizationComments
6
+import { addOne, getNumber, identity } from "shared-runtime";
7
+
8
+function Component(props) {
9
+ const x = identity(props.a);
10
+ const y = addOne(x);
11
+ const z = identity(props.b);
12
+ return [x, y, z];
13
+}
14
+
15
+export const FIXTURE_ENTRYPOINT = {
16
+ fn: Component,
17
+ params: [{ a: 1, b: 10 }],
18
+};
19
+
20
+```
21
+
22
+## Code
23
+
24
+```javascript
25
+import { unstable_useMemoCache as useMemoCache } from "react"; // @enableMemoizationComments
26
+import { addOne, getNumber, identity } from "shared-runtime";
27
+
28
+function Component(props) {
29
+ const $ = useMemoCache(9);
30
+ let t0;
31
+ let x; // "useMemo" for t0 and x:
32
+ // check if props.a changed
33
+ if ($[0] !== props.a) {
34
+ // Inputs changed, recompute
35
+ x = identity(props.a);
36
+ t0 = addOne(x);
37
+ $[0] = props.a;
38
+ $[1] = t0;
39
+ $[2] = x;
40
+ } else {
41
+ // Inputs did not change, use cached value
42
+ t0 = $[1];
43
+ x = $[2];
44
+ }
45
+ const y = t0;
46
+ let t1; // "useMemo" for t1:
47
+ // check if props.b changed
48
+ if ($[3] !== props.b) {
49
+ // Inputs changed, recompute
50
+ t1 = identity(props.b);
51
+ $[3] = props.b;
52
+ $[4] = t1;
53
+ } else {
54
+ // Inputs did not change, use cached value
55
+ t1 = $[4];
56
+ }
57
+ const z = t1;
58
+ let t2; // "useMemo" for t2:
59
+ // check if x, y, or z changed
60
+ if ($[5] !== x || $[6] !== y || $[7] !== z) {
61
+ // Inputs changed, recompute
62
+ t2 = [x, y, z];
63
+ $[5] = x;
64
+ $[6] = y;
65
+ $[7] = z;
66
+ $[8] = t2;
67
+ } else {
68
+ // Inputs did not change, use cached value
69
+ t2 = $[8];
70
+ }
71
+ return t2;
72
+}
73
+export const FIXTURE_ENTRYPOINT = { fn: Component, params: [{ a: 1, b: 10 }] };
74
+
75
+```
76
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/memoization-comments.js
new
+14
@@ -0,0 +1,14 @@
1
+// @enableMemoizationComments
2
+import { addOne, getNumber, identity } from "shared-runtime";
3
+
4
+function Component(props) {
5
+ const x = identity(props.a);
6
+ const y = addOne(x);
7
+ const z = identity(props.b);
8
+ return [x, y, z];
9
+}
10
+
11
+export const FIXTURE_ENTRYPOINT = {
12
+ fn: Component,
13
+ params: [{ a: 1, b: 10 }],
14
+};