@samitouri / QOS-React / commits / 31e128a441

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