@samitouri / QOS-React / commits / 8faed2af4c

Avoid conflicting names for reactive scope codegen

This is a key part of avoiding generating conflicting names in our output. To start, RenameVariables now returns a Set of the unique identifier names that exist in the function. Codegen uses this to avoid generating duplicate names for change variables and for the `$` useMemoCache variable. Rather than always emit `$` or `c_N`, codegen checks that this name would not conflict and appends an incrementing suffix until it finds a unique name. Note that it's still possible for us to generate conflicts with global variables, both during RenameVariable and Codegen. The next step will be to avoid conflicts with globals.

Joe Savona committed Mar 6, 2024 at 11:07 UTC 8faed2af4cfd39b32063189f3e7c845984e513ea
8 files changed +156 -27
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts
+6 -2
@@ -348,7 +348,7 @@ function* runWithEnvironment(
348 value: reactiveFunction,
349 });
350
351 - renameVariables(reactiveFunction);
351 + const uniqueIdentifiers = renameVariables(reactiveFunction);
352 yield log({
353 kind: "reactive",
354 name: "RenameVariables",
@@ -373,7 +373,11 @@ function* runWithEnvironment(
373 validatePreservedManualMemoization(reactiveFunction);
374 }
375
376 - const ast = codegenFunction(reactiveFunction, filename).unwrap();
376 + const ast = codegenFunction(
377 + reactiveFunction,
378 + uniqueIdentifiers,
379 + filename
380 + ).unwrap();
381 yield log({ kind: "ast", name: "Codegen", value: ast });
382
383 /**
compiler/packages/babel-plugin-react-forget/src/HIR/HIR.ts
+4 -4
@@ -970,9 +970,9 @@ export type Identifier = {
970 type: Type;
971 };
972
973 -export type IdentifierName =
974 - | { kind: "named"; value: ValidIdentifierName }
975 - | { kind: "promoted"; value: string };
973 +export type IdentifierName = ValidatedIdentifier | PromotedIdentifier;
974 +export type ValidatedIdentifier = { kind: "named"; value: ValidIdentifierName };
975 +export type PromotedIdentifier = { kind: "promoted"; value: string };
976
977 /**
978 * Simulated opaque type for identifier names to ensure values can only be created
@@ -988,7 +988,7 @@ export type ValidIdentifierName = string & {
988 * identifier names: only call this method for identifier names that appear in the
989 * original source code.
990 */
991 -export function makeIdentifierName(name: string): IdentifierName {
991 +export function makeIdentifierName(name: string): ValidatedIdentifier {
992 CompilerError.invariant(t.isValidIdentifier(name), {
993 reason: `Expected a valid identifier name`,
994 loc: GeneratedSource,
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+68 -11
@@ -31,7 +31,9 @@ import {
31 ReactiveValue,
32 SourceLocation,
33 SpreadPattern,
34 + ValidIdentifierName,
35 getHookKind,
36 + makeIdentifierName,
37 } from "../HIR/HIR";
38 import { printPlace } from "../HIR/PrintHIR";
39 import { eachPatternOperand } from "../HIR/visitors";
@@ -68,9 +70,15 @@ export type CodegenFunction = {
70
71 export function codegenFunction(
72 fn: ReactiveFunction,
73 + uniqueIdentifiers: Set<ValidIdentifierName>,
74 filename: string | null
75 ): Result<CodegenFunction, CompilerError> {
73 - const cx = new Context(fn.env, fn.id ?? "[[ anonymous ]]", null);
76 + const cx = new Context(
77 + fn.env,
78 + fn.id ?? "[[ anonymous ]]",
79 + uniqueIdentifiers,
80 + null
81 + );
82 const compileResult = codegenReactiveFunction(cx, fn);
83 if (compileResult.isErr()) {
84 return compileResult;
@@ -95,7 +103,7 @@ export function codegenFunction(
103 compiled.body.body.unshift(
104 t.variableDeclaration("const", [
105 t.variableDeclarator(
98 - t.identifier("$"),
106 + t.identifier(cx.synthesizeName("$")),
107 t.callExpression(t.identifier("useMemoCache"), [
108 t.numericLiteral(cacheCount),
109 ])
@@ -215,14 +223,18 @@ class Context {
223 temp: Temporaries;
224 errors: CompilerError = new CompilerError();
225 objectMethods: Map<IdentifierId, ObjectMethod> = new Map();
226 + uniqueIdentifiers: Set<ValidIdentifierName>;
227 + synthesizedNames: Map<string, ValidIdentifierName> = new Map();
228
229 constructor(
230 env: Environment,
231 fnName: string,
232 + uniqueIdentifiers: Set<ValidIdentifierName>,
233 temporaries: Temporaries | null = null
234 ) {
235 this.env = env;
236 this.fnName = fnName;
237 + this.uniqueIdentifiers = uniqueIdentifiers;
238 this.temp = temporaries !== null ? new Map(temporaries) : new Map();
239 }
240 get nextCacheIndex(): number {
@@ -236,6 +248,21 @@ class Context {
248 hasDeclared(identifier: Identifier): boolean {
249 return this.#declarations.has(identifier.id);
250 }
251 +
252 + synthesizeName(name: string): ValidIdentifierName {
253 + const previous = this.synthesizedNames.get(name);
254 + if (previous !== undefined) {
255 + return previous;
256 + }
257 + let validated = makeIdentifierName(name).value;
258 + let index = 0;
259 + while (this.uniqueIdentifiers.has(validated)) {
260 + validated = makeIdentifierName(`${name}${index++}`).value;
261 + }
262 + this.uniqueIdentifiers.add(validated);
263 + this.synthesizedNames.set(name, validated);
264 + return validated;
265 + }
266 }
267
268 function codegenBlock(cx: Context, block: ReactiveBlock): t.BlockStatement {
@@ -348,12 +375,16 @@ function codegenReactiveScope(
375 changeExpressionComments.push(printDependencyComment(dep));
376 const comparison = t.binaryExpression(
377 "!==",
351 - t.memberExpression(t.identifier("$"), t.numericLiteral(index), true),
378 + t.memberExpression(
379 + t.identifier(cx.synthesizeName("$")),
380 + t.numericLiteral(index),
381 + true
382 + ),
383 depValue
384 );
385
386 if (cx.env.config.enableChangeVariableCodegen) {
356 - const changeIdentifier = t.identifier(`c_${index}`);
387 + const changeIdentifier = t.identifier(cx.synthesizeName(`c_${index}`));
388 statements.push(
389 t.variableDeclaration("const", [
390 t.variableDeclarator(changeIdentifier, comparison),
@@ -367,7 +398,11 @@ function codegenReactiveScope(
398 t.expressionStatement(
399 t.assignmentExpression(
400 "=",
370 - t.memberExpression(t.identifier("$"), t.numericLiteral(index), true),
401 + t.memberExpression(
402 + t.identifier(cx.synthesizeName("$")),
403 + t.numericLiteral(index),
404 + true
405 + ),
406 depValue
407 )
408 )
@@ -398,7 +433,11 @@ function codegenReactiveScope(
433 t.expressionStatement(
434 t.assignmentExpression(
435 "=",
401 - t.memberExpression(t.identifier("$"), t.numericLiteral(index), true),
436 + t.memberExpression(
437 + t.identifier(cx.synthesizeName("$")),
438 + t.numericLiteral(index),
439 + true
440 + ),
441 wrapCacheDep(cx, name)
442 )
443 )
@@ -408,7 +447,11 @@ function codegenReactiveScope(
447 t.assignmentExpression(
448 "=",
449 name,
411 - t.memberExpression(t.identifier("$"), t.numericLiteral(index), true)
450 + t.memberExpression(
451 + t.identifier(cx.synthesizeName("$")),
452 + t.numericLiteral(index),
453 + true
454 + )
455 )
456 )
457 );
@@ -426,7 +469,11 @@ function codegenReactiveScope(
469 t.expressionStatement(
470 t.assignmentExpression(
471 "=",
429 - t.memberExpression(t.identifier("$"), t.numericLiteral(index), true),
472 + t.memberExpression(
473 + t.identifier(cx.synthesizeName("$")),
474 + t.numericLiteral(index),
475 + true
476 + ),
477 wrapCacheDep(cx, name)
478 )
479 )
@@ -436,7 +483,11 @@ function codegenReactiveScope(
483 t.assignmentExpression(
484 "=",
485 name,
439 - t.memberExpression(t.identifier("$"), t.numericLiteral(index), true)
486 + t.memberExpression(
487 + t.identifier(cx.synthesizeName("$")),
488 + t.numericLiteral(index),
489 + true
490 + )
491 )
492 )
493 );
@@ -460,7 +511,7 @@ function codegenReactiveScope(
511 testCondition = t.binaryExpression(
512 "===",
513 t.memberExpression(
463 - t.identifier("$"),
514 + t.identifier(cx.synthesizeName("$")),
515 t.numericLiteral(firstOutputIndex),
516 true
517 ),
@@ -1341,6 +1392,7 @@ function codegenInstructionValue(
1392 new Context(
1393 cx.env,
1394 reactiveFunction.id ?? "[[ anonymous ]]",
1395 + cx.uniqueIdentifiers,
1396 cx.temp
1397 ),
1398 reactiveFunction
@@ -1543,7 +1595,12 @@ function codegenInstructionValue(
1595 pruneUnusedLValues(reactiveFunction);
1596 pruneHoistedContexts(reactiveFunction);
1597 const fn = codegenReactiveFunction(
1546 - new Context(cx.env, reactiveFunction.id ?? "[[ anonymous ]]", cx.temp),
1598 + new Context(
1599 + cx.env,
1600 + reactiveFunction.id ?? "[[ anonymous ]]",
1601 + cx.uniqueIdentifiers,
1602 + cx.temp
1603 + ),
1604 reactiveFunction
1605 ).unwrap();
1606 if (instrValue.expr.type === "ArrowFunctionExpression") {
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/RenameVariables.ts
+10 -2
@@ -16,6 +16,7 @@ import {
16 ReactiveFunction,
17 ReactiveScopeBlock,
18 ReactiveValue,
19 + ValidIdentifierName,
20 isPromotedJsxTemporary,
21 isPromotedTemporary,
22 makeIdentifierName,
@@ -39,10 +40,15 @@ import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";
40 * For temporary values that are promoted to named variables, the starting name
41 * is "T0" for values that appear in JSX tag position and "t0" otherwise. If this
42 * name conflicts, the number portion increments until the name is unique (t1, t2, etc).
43 + *
44 + * Returns a Set of all the unique variable names in the function after renaming.
45 */
43 -export function renameVariables(fn: ReactiveFunction): void {
46 +export function renameVariables(
47 + fn: ReactiveFunction
48 +): Set<ValidIdentifierName> {
49 const scopes = new Scopes();
50 renameVariablesImpl(fn, new Visitor(), scopes);
51 + return scopes.names;
52 }
53
54 function renameVariablesImpl(
@@ -109,6 +115,7 @@ class Visitor extends ReactiveFunctionVisitor<Scopes> {
115 class Scopes {
116 #seen: Map<IdentifierId, IdentifierName> = new Map();
117 #stack: Array<Map<string, IdentifierId>> = [new Map()];
118 + names: Set<ValidIdentifierName> = new Set();
119
120 visit(identifier: Identifier): void {
121 const originalName = identifier.name;
@@ -134,7 +141,7 @@ class Scopes {
141 } else if (isPromotedJsxTemporary(originalName.value)) {
142 name = `T${id++}`;
143 } else {
137 - name = `${identifier.name}$${id++}`;
144 + name = `${originalName.value}$${id++}`;
145 }
146 previous = this.#lookup(name);
147 }
@@ -142,6 +149,7 @@ class Scopes {
149 identifier.name = identifierName;
150 this.#seen.set(identifier.id, identifierName);
151 this.#stack.at(-1)!.set(identifierName.value, identifier.id);
152 + this.names.add(identifierName.value);
153 }
154
155 #lookup(name: string): IdentifierId | null {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/conflicting-dollar-sign-variable.expect.md new
+48
@@ -0,0 +1,48 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { identity } from "shared-runtime";
6 +
7 +function Component(props) {
8 + const $ = identity("jQuery");
9 + const t0 = identity([$]);
10 + return t0;
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Component,
15 + params: [{}],
16 +};
17 +
18 +```
19 +
20 +## Code
21 +
22 +```javascript
23 +import { unstable_useMemoCache as useMemoCache } from "react";
24 +import { identity } from "shared-runtime";
25 +
26 +function Component(props) {
27 + const $0 = useMemoCache(1);
28 + let t0;
29 + if ($0[0] === Symbol.for("react.memo_cache_sentinel")) {
30 + const $ = identity("jQuery");
31 + t0 = identity([$]);
32 + $0[0] = t0;
33 + } else {
34 + t0 = $0[0];
35 + }
36 + const t0$0 = t0;
37 + return t0$0;
38 +}
39 +
40 +export const FIXTURE_ENTRYPOINT = {
41 + fn: Component,
42 + params: [{}],
43 +};
44 +
45 +```
46 +
47 +### Eval output
48 +(kind: ok) ["jQuery"]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/conflicting-dollar-sign-variable.js new
+12
@@ -0,0 +1,12 @@
1 +import { identity } from "shared-runtime";
2 +
3 +function Component(props) {
4 + const $ = identity("jQuery");
5 + const t0 = identity([$]);
6 + return t0;
7 +}
8 +
9 +export const FIXTURE_ENTRYPOINT = {
10 + fn: Component,
11 + params: [{}],
12 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/option-enable-change-variable-codegen.expect.md
+6 -6
@@ -4,8 +4,8 @@
4 ```javascript
5 // @enableChangeVariableCodegen
6 function Component(props) {
7 - const x = [props.a, props.b.c];
8 - return x;
7 + const c_0 = [props.a, props.b.c];
8 + return c_0;
9 }
10
11 export const FIXTURE_ENTRYPOINT = {
@@ -21,10 +21,10 @@ export const FIXTURE_ENTRYPOINT = {
21 import { unstable_useMemoCache as useMemoCache } from "react"; // @enableChangeVariableCodegen
22 function Component(props) {
23 const $ = useMemoCache(3);
24 - const c_0 = $[0] !== props.a;
24 + const c_00 = $[0] !== props.a;
25 const c_1 = $[1] !== props.b.c;
26 let t0;
27 - if (c_0 || c_1) {
27 + if (c_00 || c_1) {
28 t0 = [props.a, props.b.c];
29 $[0] = props.a;
30 $[1] = props.b.c;
@@ -32,8 +32,8 @@ function Component(props) {
32 } else {
33 t0 = $[2];
34 }
35 - const x = t0;
36 - return x;
35 + const c_0 = t0;
36 + return c_0;
37 }
38
39 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/option-enable-change-variable-codegen.js
+2 -2
@@ -1,7 +1,7 @@
1 // @enableChangeVariableCodegen
2 function Component(props) {
3 - const x = [props.a, props.b.c];
4 - return x;
3 + const c_0 = [props.a, props.b.c];
4 + return c_0;
5 }
6
7 export const FIXTURE_ENTRYPOINT = {