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 = {