Use opaque type for Identifier.name for improved correctness
Uses an enum for Identifier.name to distinguish originally named identifiers vs promoted temporaries. An opaque type for the named identifier variant makes it hard to accidentally create that type.
Joe Savona committed
Mar 6, 2024 at 11:07 UTC
31e128a4413e2902e5fbfda5e5f6e9fdfc1e7619
13 files changed
+150
-53
compiler/packages/babel-plugin-react-forget/src/HIR/HIR.ts
+77
-1
@@ -959,7 +959,7 @@ export type Identifier = {
959
*/
960
id: IdentifierId;
961
// null for temporaries. name is primarily used for debugging.
962
- name: string | null;
962
+ name: IdentifierName | null;
963
// The range for which this variable is mutable
964
mutableRange: MutableRange;
965
/*
@@ -970,6 +970,82 @@ export type Identifier = {
970
type: Type;
971
};
972
973
+export type IdentifierName =
974
+ | { kind: "named"; value: ValidIdentifierName }
975
+ | { kind: "promoted"; value: string };
976
+
977
+/**
978
+ * Simulated opaque type for identifier names to ensure values can only be created
979
+ * through the below helpers.
980
+ */
981
+const opaqueValidIdentifierName = Symbol();
982
+export type ValidIdentifierName = string & {
983
+ [opaqueValidIdentifierName]: "ValidIdentifierName";
984
+};
985
+
986
+/**
987
+ * Creates a valid identifier name. This should *not* be used for synthesizing
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 {
992
+ CompilerError.invariant(t.isValidIdentifier(name), {
993
+ reason: `Expected a valid identifier name`,
994
+ loc: GeneratedSource,
995
+ description: `'${name}' is not a valid JavaScript identifier`,
996
+ suggestions: null,
997
+ });
998
+ return {
999
+ kind: "named",
1000
+ value: name as ValidIdentifierName,
1001
+ };
1002
+}
1003
+
1004
+/**
1005
+ * Given an unnamed identifier, promote it to a named identifier.
1006
+ */
1007
+export function promoteTemporaryToNamedIdentifier(
1008
+ identifier: Identifier
1009
+): void {
1010
+ CompilerError.invariant(identifier.name === null, {
1011
+ reason: `Expected a temporary (unnamed) identifier`,
1012
+ loc: GeneratedSource,
1013
+ description: `Identifier already has a name, '${identifier.name}'`,
1014
+ suggestions: null,
1015
+ });
1016
+ identifier.name = {
1017
+ kind: "promoted",
1018
+ value: `#t${identifier.id}`,
1019
+ };
1020
+}
1021
+
1022
+export function isPromotedTemporary(name: string): boolean {
1023
+ return name.startsWith("#t");
1024
+}
1025
+
1026
+/**
1027
+ * Given an unnamed identifier, promote it to a named identifier, distinguishing
1028
+ * it as a value that needs to be capitalized since it appears in JSX element tag position
1029
+ */
1030
+export function promoteTemporaryJsxTagToNamedIdentifier(
1031
+ identifier: Identifier
1032
+): void {
1033
+ CompilerError.invariant(identifier.name === null, {
1034
+ reason: `Expected a temporary (unnamed) identifier`,
1035
+ loc: GeneratedSource,
1036
+ description: `Identifier already has a name, '${identifier.name}'`,
1037
+ suggestions: null,
1038
+ });
1039
+ identifier.name = {
1040
+ kind: "promoted",
1041
+ value: `#T${identifier.id}`,
1042
+ };
1043
+}
1044
+
1045
+export function isPromotedJsxTemporary(name: string): boolean {
1046
+ return name.startsWith("#T");
1047
+}
1048
+
1049
export type AbstractValue = {
1050
kind: ValueKind;
1051
reason: ReadonlySet<ValueReason>;
compiler/packages/babel-plugin-react-forget/src/HIR/HIRBuilder.ts
+4
-3
@@ -25,6 +25,7 @@ import {
25
Place,
26
Terminal,
27
makeBlockId,
28
+ makeIdentifierName,
29
makeInstructionId,
30
makeType,
31
} from "./HIR";
@@ -260,8 +261,8 @@ export default class HIRBuilder {
261
return null;
262
}
263
const resolvedBinding = this.resolveBinding(babelBinding.identifier);
263
- if (resolvedBinding.name && resolvedBinding.name !== originalName) {
264
- babelBinding.scope.rename(originalName, resolvedBinding.name);
264
+ if (resolvedBinding.name && resolvedBinding.name.value !== originalName) {
265
+ babelBinding.scope.rename(originalName, resolvedBinding.name.value);
266
}
267
return resolvedBinding;
268
}
@@ -285,7 +286,7 @@ export default class HIRBuilder {
286
const id = this.nextIdentifierId;
287
const identifier: Identifier = {
288
id,
288
- name,
289
+ name: makeIdentifierName(name),
290
mutableRange: {
291
start: makeInstructionId(0),
292
end: makeInstructionId(0),
compiler/packages/babel-plugin-react-forget/src/HIR/PrintHIR.ts
+6
-2
@@ -15,6 +15,7 @@ import type {
15
HIR,
16
HIRFunction,
17
Identifier,
18
+ IdentifierName,
19
Instruction,
20
InstructionValue,
21
LValue,
@@ -724,8 +725,11 @@ export function printIdentifier(id: Identifier): string {
725
return `${printName(id.name)}\$${id.id}${printScope(id.scope)}`;
726
}
727
727
-function printName(name: string | null): string {
728
- return name ?? "";
728
+function printName(name: IdentifierName | null): string {
729
+ if (name === null) {
730
+ return "";
731
+ }
732
+ return name.value;
733
}
734
735
function printScope(scope: ReactiveScope | null): string {
compiler/packages/babel-plugin-react-forget/src/Inference/AnalyseFunctions.ts
+5
-4
@@ -10,6 +10,7 @@ import {
10
Effect,
11
HIRFunction,
12
Identifier,
13
+ IdentifierName,
14
LoweredFunction,
15
Place,
16
ReactiveScopeDependency,
@@ -123,13 +124,13 @@ function infer(
124
isMutatedOrReassigned(operand.identifier) &&
125
operand.identifier.name !== null
126
) {
126
- mutations.set(operand.identifier.name, operand.effect);
127
+ mutations.set(operand.identifier.name.value, operand.effect);
128
}
129
operand.identifier.mutableRange.end = operand.identifier.mutableRange.start;
130
}
131
132
for (const dep of loweredFunc.dependencies) {
132
- let name: string | null = null;
133
+ let name: IdentifierName | null = null;
134
135
if (state.properties.has(dep.identifier)) {
136
const receiver = state.properties.get(dep.identifier)!;
@@ -148,7 +149,7 @@ function infer(
149
*/
150
dep.effect = Effect.Capture;
151
} else if (name !== null) {
151
- const effect = mutations.get(name);
152
+ const effect = mutations.get(name.value);
153
if (effect !== undefined) {
154
dep.effect = effect === Effect.Unknown ? Effect.Capture : effect;
155
}
@@ -171,7 +172,7 @@ function infer(
172
suggestions: null,
173
});
174
174
- const effect = mutations.get(place.identifier.name);
175
+ const effect = mutations.get(place.identifier.name.value);
176
if (effect !== undefined) {
177
place.effect = effect === Effect.Unknown ? Effect.Capture : effect;
178
loweredFunc.dependencies.push(place);
compiler/packages/babel-plugin-react-forget/src/Inference/InlineImmediatelyInvokedFunctionExpressions.ts
+2
-6
@@ -14,13 +14,13 @@ import {
14
GeneratedSource,
15
GotoVariant,
16
HIRFunction,
17
- Identifier,
17
IdentifierId,
18
InstructionKind,
19
LabelTerminal,
20
Place,
21
makeInstructionId,
22
makeType,
23
+ promoteTemporaryToNamedIdentifier,
24
reversePostorderBlocks,
25
} from "../HIR";
26
import { markInstructionIds, markPredecessors } from "../HIR/HIRBuilder";
@@ -159,7 +159,7 @@ export function inlineImmediatelyInvokedFunctionExpressions(
159
declareTemporary(fn.env, block, result);
160
161
// Promote the temporary with a name as we require this to persist
162
- promoteTemporary(result.identifier);
162
+ promoteTemporaryToNamedIdentifier(result.identifier);
163
164
/*
165
* Rewrite blocks from the lambda to replace any `return` with a
@@ -293,7 +293,3 @@ function declareTemporary(
293
},
294
});
295
}
296
-
297
-function promoteTemporary(temp: Identifier): void {
298
- temp.name = `#t${temp.id}`;
299
-}
compiler/packages/babel-plugin-react-forget/src/Optimization/DeadCodeElimination.ts
+2
-2
@@ -68,7 +68,7 @@ class State {
68
reference(identifier: Identifier): void {
69
this.identifiers.add(identifier.id);
70
if (identifier.name !== null) {
71
- this.named.add(identifier.name);
71
+ this.named.add(identifier.name.value);
72
}
73
}
74
@@ -80,7 +80,7 @@ class State {
80
isIdOrNameUsed(identifier: Identifier): boolean {
81
return (
82
this.identifiers.has(identifier.id) ||
83
- (identifier.name !== null && this.named.has(identifier.name))
83
+ (identifier.name !== null && this.named.has(identifier.name.value))
84
);
85
}
86
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+5
-3
@@ -541,14 +541,16 @@ function codegenReactiveScope(
541
t.ifStatement(
542
t.binaryExpression(
543
"!==",
544
- t.identifier(scope.earlyReturnValue.value.name!),
544
+ t.identifier(scope.earlyReturnValue.value.name!.value),
545
t.callExpression(
546
t.memberExpression(t.identifier("Symbol"), t.identifier("for")),
547
[t.stringLiteral(EARLY_RETURN_SENTINEL)]
548
)
549
),
550
t.blockStatement([
551
- t.returnStatement(t.identifier(scope.earlyReturnValue.value.name!)),
551
+ t.returnStatement(
552
+ t.identifier(scope.earlyReturnValue.value.name!.value)
553
+ ),
554
])
555
)
556
);
@@ -2009,7 +2011,7 @@ function codegenPlace(cx: Context, place: Place): t.Expression | t.JSXText {
2011
2012
function convertIdentifier(identifier: Identifier): t.Identifier {
2013
if (identifier.name !== null) {
2012
- return t.identifier(`${identifier.name}`);
2014
+ return t.identifier(identifier.name.value);
2015
}
2016
return t.identifier(`t${identifier.id}`);
2017
}
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/ExtractScopeDeclarationsFromDestructuring.ts
+7
-1
@@ -15,6 +15,7 @@ import {
15
ReactiveInstruction,
16
ReactiveScopeBlock,
17
ReactiveStatement,
18
+ promoteTemporaryToNamedIdentifier,
19
} from "../HIR";
20
import { eachPatternOperand, mapPatternOperands } from "../HIR/visitors";
21
import {
@@ -152,8 +153,13 @@ function transformDestructuring(
153
const tempId = state.env.nextIdentifierId;
154
const temporary = {
155
...place,
155
- identifier: { ...place.identifier, id: tempId, name: `#t${tempId}` },
156
+ identifier: {
157
+ ...place.identifier,
158
+ id: tempId,
159
+ name: null, // overwritten below
160
+ },
161
};
162
+ promoteTemporaryToNamedIdentifier(temporary.identifier);
163
renamed.set(place, temporary);
164
return temporary;
165
});
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PromoteUsedTemporaries.ts
+4
-4
@@ -15,11 +15,12 @@ import {
15
ReactiveInstruction,
16
ReactiveScopeBlock,
17
ReactiveValue,
18
+ promoteTemporaryJsxTagToNamedIdentifier,
19
+ promoteTemporaryToNamedIdentifier,
20
} from "../HIR/HIR";
21
import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";
22
23
type VisitorState = {
22
- nextId: number;
24
tags: JsxExpressionTags;
25
};
26
class Visitor extends ReactiveFunctionVisitor<VisitorState> {
@@ -70,7 +71,6 @@ export function promoteUsedTemporaries(fn: ReactiveFunction): void {
71
const tags: JsxExpressionTags = new Set();
72
visitReactiveFunction(fn, new CollectJsxTagsVisitor(), tags);
73
const state: VisitorState = {
73
- nextId: 0,
74
tags,
75
};
76
visitReactiveFunction(fn, new Visitor(), state);
@@ -85,8 +85,8 @@ function promoteTemporary(identifier: Identifier, state: VisitorState): void {
85
suggestions: null,
86
});
87
if (state.tags.has(identifier.id)) {
88
- identifier.name = `#T${state.nextId++}`;
88
+ promoteTemporaryJsxTagToNamedIdentifier(identifier);
89
} else {
90
- identifier.name = `#t${state.nextId++}`;
90
+ promoteTemporaryToNamedIdentifier(identifier);
91
}
92
}
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PropagateEarlyReturns.ts
+2
-1
@@ -16,6 +16,7 @@ import {
16
ReactiveStatement,
17
ReactiveTerminalStatement,
18
makeInstructionId,
19
+ promoteTemporaryToNamedIdentifier,
20
} from "../HIR";
21
import { createTemporaryPlace } from "../HIR/HIRBuilder";
22
import { EARLY_RETURN_SENTINEL } from "./CodegenReactiveFunction";
@@ -274,7 +275,7 @@ class Transform extends ReactiveFunctionTransform<State> {
275
earlyReturnValue = state.earlyReturnValue;
276
} else {
277
const identifier = createTemporaryPlace(this.env).identifier;
277
- identifier.name = `#t${identifier.id}`;
278
+ promoteTemporaryToNamedIdentifier(identifier);
279
earlyReturnValue = {
280
label: this.env.nextBlockId,
281
loc,
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/RenameVariables.ts
+14
-9
@@ -9,11 +9,15 @@ import { CompilerError } from "../CompilerError";
9
import {
10
Identifier,
11
IdentifierId,
12
+ IdentifierName,
13
InstructionId,
14
Place,
15
ReactiveBlock,
16
ReactiveFunction,
17
ReactiveScopeBlock,
18
+ isPromotedJsxTemporary,
19
+ isPromotedTemporary,
20
+ makeIdentifierName,
21
} from "../HIR/HIR";
22
import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";
23
@@ -88,7 +92,7 @@ class Visitor extends ReactiveFunctionVisitor<Scopes> {
92
}
93
94
class Scopes {
91
- #seen: Map<IdentifierId, string> = new Map();
95
+ #seen: Map<IdentifierId, IdentifierName> = new Map();
96
#stack: Array<Map<string, IdentifierId>> = [new Map()];
97
98
visit(identifier: Identifier): void {
@@ -101,27 +105,28 @@ class Scopes {
105
identifier.name = mappedName;
106
return;
107
}
104
- let name = originalName;
108
+ let name: string = originalName.value;
109
let id = 0;
106
- if (name.startsWith("#t")) {
110
+ if (isPromotedTemporary(originalName.value)) {
111
name = `t${id++}`;
108
- } else if (name.startsWith("#T")) {
112
+ } else if (isPromotedJsxTemporary(originalName.value)) {
113
name = `T${id++}`;
114
}
115
let previous = this.#lookup(name);
116
while (previous !== null) {
113
- if (originalName.startsWith("#t")) {
117
+ if (isPromotedTemporary(originalName.value)) {
118
name = `t${id++}`;
115
- } else if (originalName.startsWith("#T")) {
119
+ } else if (isPromotedJsxTemporary(originalName.value)) {
120
name = `T${id++}`;
121
} else {
122
name = `${identifier.name}$${id++}`;
123
}
124
previous = this.#lookup(name);
125
}
122
- identifier.name = name;
123
- this.#seen.set(identifier.id, name);
124
- this.#stack.at(-1)!.set(name, identifier.id);
126
+ const identifierName = makeIdentifierName(name);
127
+ identifier.name = identifierName;
128
+ this.#seen.set(identifier.id, identifierName);
129
+ this.#stack.at(-1)!.set(identifierName.value, identifier.id);
130
}
131
132
#lookup(name: string): IdentifierId | null {
compiler/packages/babel-plugin-react-forget/src/SSA/LeaveSSA.ts
+16
-14
@@ -99,7 +99,7 @@ export function leaveSSA(fn: HIRFunction): void {
99
for (const param of fn.params) {
100
let place: Place = param.kind === "Identifier" ? param : param.place;
101
if (place.identifier.name !== null) {
102
- declarations.set(place.identifier.name, {
102
+ declarations.set(place.identifier.name.value, {
103
lvalue: {
104
kind: InstructionKind.Let,
105
place,
@@ -145,13 +145,13 @@ export function leaveSSA(fn: HIRFunction): void {
145
if (value.kind === "DeclareLocal") {
146
const name = value.lvalue.place.identifier.name;
147
if (name !== null) {
148
- CompilerError.invariant(!declarations.has(name), {
148
+ CompilerError.invariant(!declarations.has(name.value), {
149
reason: `Unexpected duplicate declaration`,
150
- description: `Found duplicate declaration for '${name}'`,
150
+ description: `Found duplicate declaration for '${name.value}'`,
151
loc: value.lvalue.place.loc,
152
suggestions: null,
153
});
154
- declarations.set(name, {
154
+ declarations.set(name.value, {
155
lvalue: value.lvalue,
156
place: value.lvalue.place,
157
});
@@ -166,7 +166,9 @@ export function leaveSSA(fn: HIRFunction): void {
166
loc: value.lvalue.loc,
167
suggestions: null,
168
});
169
- const originalLVal = declarations.get(value.lvalue.identifier.name);
169
+ const originalLVal = declarations.get(
170
+ value.lvalue.identifier.name.value
171
+ );
172
CompilerError.invariant(originalLVal !== undefined, {
173
reason: `Expected update expression to be applied to a previously defined variable`,
174
description: null,
@@ -177,7 +179,7 @@ export function leaveSSA(fn: HIRFunction): void {
179
} else if (value.kind === "StoreLocal") {
180
if (value.lvalue.place.identifier.name != null) {
181
const originalLVal = declarations.get(
180
- value.lvalue.place.identifier.name
182
+ value.lvalue.place.identifier.name.value
183
);
184
if (
185
originalLVal === undefined ||
@@ -194,7 +196,7 @@ export function leaveSSA(fn: HIRFunction): void {
196
suggestions: null,
197
}
198
);
197
- declarations.set(value.lvalue.place.identifier.name, {
199
+ declarations.set(value.lvalue.place.identifier.name.value, {
200
lvalue: value.lvalue,
201
place: value.lvalue.place,
202
});
@@ -227,7 +229,7 @@ export function leaveSSA(fn: HIRFunction): void {
229
);
230
kind = InstructionKind.Const;
231
} else {
230
- const originalLVal = declarations.get(place.identifier.name);
232
+ const originalLVal = declarations.get(place.identifier.name.value);
233
if (
234
originalLVal === undefined ||
235
originalLVal.lvalue === value.lvalue
@@ -241,7 +243,7 @@ export function leaveSSA(fn: HIRFunction): void {
243
suggestions: null,
244
}
245
);
244
- declarations.set(place.identifier.name, {
246
+ declarations.set(place.identifier.name.value, {
247
lvalue: value.lvalue,
248
place,
249
});
@@ -388,10 +390,10 @@ export function leaveSSA(fn: HIRFunction): void {
390
const value = initIdentifier.value;
391
if (value.lvalue.place.identifier.name !== null) {
392
const originalLVal = declarations.get(
391
- value.lvalue.place.identifier.name
393
+ value.lvalue.place.identifier.name.value
394
);
395
if (originalLVal === undefined) {
394
- declarations.set(value.lvalue.place.identifier.name, {
396
+ declarations.set(value.lvalue.place.identifier.name.value, {
397
lvalue: value.lvalue,
398
place: value.lvalue.place,
399
});
@@ -440,7 +442,7 @@ export function leaveSSA(fn: HIRFunction): void {
442
loc: null,
443
suggestions: null,
444
});
443
- const declaration = declarations.get(phi.id.name);
445
+ const declaration = declarations.get(phi.id.name.value);
446
CompilerError.invariant(declaration != null, {
447
loc: null,
448
reason: "Expected a declaration for all variables",
@@ -480,7 +482,7 @@ export function leaveSSA(fn: HIRFunction): void {
482
rewrites.set(phi.id, canonicalId);
483
484
if (canonicalId.name !== null) {
483
- const declaration = declarations.get(canonicalId.name);
485
+ const declaration = declarations.get(canonicalId.name.value);
486
if (declaration !== undefined) {
487
declaration.lvalue.kind = InstructionKind.Let;
488
}
@@ -512,7 +514,7 @@ function rewritePlace(
514
if (nextIdentifier === prevIdentifier) return;
515
place.identifier = nextIdentifier;
516
} else if (prevIdentifier.name != null) {
515
- const declaration = declarations.get(prevIdentifier.name);
517
+ const declaration = declarations.get(prevIdentifier.name.value);
518
// Only rewrite identifiers that were declared within the function
519
if (declaration === undefined) return;
520
const originalIdentifier = declaration.place.identifier;
compiler/packages/babel-plugin-react-forget/src/Validation/ValidateHooksUsage.ts
+6
-3
@@ -152,7 +152,10 @@ export function validateHooksUsage(fn: HIRFunction): void {
152
const valueKinds = new Map<IdentifierId, Kind>();
153
function getKindForPlace(place: Place): Kind {
154
const knownKind = valueKinds.get(place.identifier.id);
155
- if (place.identifier.name !== null && isHookName(place.identifier.name)) {
155
+ if (
156
+ place.identifier.name !== null &&
157
+ isHookName(place.identifier.name.value)
158
+ ) {
159
return joinKinds(knownKind ?? Kind.Local, Kind.PotentialHook);
160
} else {
161
return knownKind ?? Kind.Local;
@@ -179,7 +182,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
182
for (const [, block] of fn.body.blocks) {
183
for (const phi of block.phis) {
184
let kind: Kind =
182
- phi.id.name !== null && isHookName(phi.id.name)
185
+ phi.id.name !== null && isHookName(phi.id.name.value)
186
? Kind.PotentialHook
187
: Kind.Local;
188
for (const [, operand] of phi.operands) {
@@ -333,7 +336,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
336
for (const lvalue of eachInstructionLValue(instr)) {
337
const isHookProperty =
338
lvalue.identifier.name !== null &&
336
- isHookName(lvalue.identifier.name);
339
+ isHookName(lvalue.identifier.name.value);
340
let kind: Kind;
341
switch (objectKind) {
342
case Kind.Error: {