1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 */78import {CompilerError} from '..';9import {10 convertHoistedLValueKind,11 IdentifierId,12 InstructionId,13 InstructionKind,14 Place,15 ReactiveFunction,16 ReactiveInstruction,17 ReactiveScopeBlock,18 ReactiveStatement,19} from '../HIR';20import {empty, Stack} from '../Utils/Stack';21import {22 ReactiveFunctionTransform,23 Transformed,24 visitReactiveFunction,25} from './visitors';2627/*28 * Prunes DeclareContexts lowered for HoistedConsts, and transforms any references back to its29 * original instruction kind.30 *31 * Also detects and bails out on context variables which are:32 * - function declarations, which are hoisted by JS engines to the nearest block scope33 * - referenced before they are defined (i.e. having a `DeclareContext HoistedConst`)34 * - declared35 *36 * This is because React Compiler converts a `function foo()` function declaration to37 * 1. a `let foo;` declaration before reactive memo blocks38 * 2. a `foo = function foo() {}` assignment within the block39 *40 * This means references before the assignment are invalid (see fixture41 * error.todo-functiondecl-hoisting)42 */43export function pruneHoistedContexts(fn: ReactiveFunction): void {44 visitReactiveFunction(fn, new Visitor(), {45 activeScopes: empty(),46 uninitialized: new Map(),47 });48}4950type VisitorState = {51 activeScopes: Stack<Set<IdentifierId>>;52 uninitialized: Map<53 IdentifierId,54 | {55 kind: 'unknown-kind';56 }57 | {58 kind: 'func';59 definition: Place | null;60 }61 >;62};6364class Visitor extends ReactiveFunctionTransform<VisitorState> {65 override visitScope(scope: ReactiveScopeBlock, state: VisitorState): void {66 state.activeScopes = state.activeScopes.push(67 new Set(scope.scope.declarations.keys()),68 );69 /**70 * Add declared but not initialized / assigned variables. This may include71 * function declarations that escape the memo block.72 */73 for (const decl of scope.scope.declarations.values()) {74 state.uninitialized.set(decl.identifier.id, {kind: 'unknown-kind'});75 }76 this.traverseScope(scope, state);77 state.activeScopes.pop();78 for (const decl of scope.scope.declarations.values()) {79 state.uninitialized.delete(decl.identifier.id);80 }81 }82 override visitPlace(83 _id: InstructionId,84 place: Place,85 state: VisitorState,86 ): void {87 const maybeHoistedFn = state.uninitialized.get(place.identifier.id);88 if (89 maybeHoistedFn?.kind === 'func' &&90 maybeHoistedFn.definition !== place91 ) {92 CompilerError.throwTodo({93 reason: '[PruneHoistedContexts] Rewrite hoisted function references',94 loc: place.loc,95 });96 }97 }98 override transformInstruction(99 instruction: ReactiveInstruction,100 state: VisitorState,101 ): Transformed<ReactiveStatement> {102 /**103 * Remove hoisted declarations to preserve TDZ104 */105 if (instruction.value.kind === 'DeclareContext') {106 const maybeNonHoisted = convertHoistedLValueKind(107 instruction.value.lvalue.kind,108 );109 if (maybeNonHoisted != null) {110 if (111 maybeNonHoisted === InstructionKind.Function &&112 state.uninitialized.has(instruction.value.lvalue.place.identifier.id)113 ) {114 state.uninitialized.set(115 instruction.value.lvalue.place.identifier.id,116 {117 kind: 'func',118 definition: null,119 },120 );121 }122 return {kind: 'remove'};123 }124 }125 if (126 instruction.value.kind === 'StoreContext' &&127 instruction.value.lvalue.kind !== InstructionKind.Reassign128 ) {129 /**130 * Rewrite StoreContexts let/const that will be pre-declared in131 * codegen to reassignments.132 */133 const lvalueId = instruction.value.lvalue.place.identifier.id;134 const isDeclaredByScope = state.activeScopes.find(scope =>135 scope.has(lvalueId),136 );137 if (isDeclaredByScope) {138 if (139 instruction.value.lvalue.kind === InstructionKind.Let ||140 instruction.value.lvalue.kind === InstructionKind.Const141 ) {142 instruction.value.lvalue.kind = InstructionKind.Reassign;143 } else if (instruction.value.lvalue.kind === InstructionKind.Function) {144 const maybeHoistedFn = state.uninitialized.get(lvalueId);145 if (maybeHoistedFn != null) {146 CompilerError.invariant(maybeHoistedFn.kind === 'func', {147 reason: '[PruneHoistedContexts] Unexpected hoisted function',148 loc: instruction.loc,149 });150 maybeHoistedFn.definition = instruction.value.lvalue.place;151 /**152 * References to hoisted functions are now "safe" as variable assignments153 * have finished.154 */155 state.uninitialized.delete(lvalueId);156 }157 } else {158 CompilerError.throwTodo({159 reason: '[PruneHoistedContexts] Unexpected kind',160 description: `(${instruction.value.lvalue.kind})`,161 loc: instruction.loc,162 });163 }164 }165 }166167 this.visitInstruction(instruction, state);168 return {kind: 'keep'};169 }170}
Findings
✓ No findings reported for this file.