main
ts 2,479 lines 75.9 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import * as t from '@babel/types';
9 import {createHmac} from 'crypto';
10 import {
11 pruneHoistedContexts,
12 pruneUnusedLValues,
13 pruneUnusedLabels,
14 renameVariables,
15 } from '.';
16 import {
17 CompilerError,
18 CompilerErrorDetail,
19 ErrorCategory,
20 } from '../CompilerError';
21 import {Environment, ExternalFunction} from '../HIR';
22 import {
23 ArrayPattern,
24 BlockId,
25 DeclarationId,
26 GeneratedSource,
27 Identifier,
28 IdentifierId,
29 InstructionKind,
30 JsxAttribute,
31 ObjectMethod,
32 ObjectPropertyKey,
33 Pattern,
34 Place,
35 PrunedReactiveScopeBlock,
36 ReactiveBlock,
37 ReactiveFunction,
38 ReactiveInstruction,
39 ReactiveScope,
40 ReactiveScopeBlock,
41 ReactiveScopeDeclaration,
42 ReactiveScopeDependency,
43 ReactiveTerminal,
44 ReactiveValue,
45 SourceLocation,
46 SpreadPattern,
47 ValidIdentifierName,
48 getHookKind,
49 makeIdentifierName,
50 } from '../HIR/HIR';
51 import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
52 import {eachPatternOperand} from '../HIR/visitors';
53
54 import {GuardKind} from '../Utils/RuntimeDiagnosticConstants';
55 import {assertExhaustive} from '../Utils/utils';
56 import {buildReactiveFunction} from './BuildReactiveFunction';
57 import {SINGLE_CHILD_FBT_TAGS} from './MemoizeFbtAndMacroOperandsInSameScope';
58 import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
59 import {ReactFunctionType} from '../HIR/Environment';
60 import {ProgramContext} from '../Entrypoint';
61
62 export const MEMO_CACHE_SENTINEL = 'react.memo_cache_sentinel';
63 export const EARLY_RETURN_SENTINEL = 'react.early_return_sentinel';
64
65 export type CodegenFunction = {
66 type: 'CodegenFunction';
67 id: t.Identifier | null;
68 nameHint: string | null;
69 params: t.FunctionDeclaration['params'];
70 body: t.BlockStatement;
71 generator: boolean;
72 async: boolean;
73 loc: SourceLocation;
74
75 /*
76 * Compiler info for logging and heuristics
77 * Number of memo slots (value passed to useMemoCache)
78 */
79 memoSlotsUsed: number;
80 /*
81 * Number of memo *blocks* (reactive scopes) regardless of
82 * how many inputs/outputs each block has
83 */
84 memoBlocks: number;
85
86 /**
87 * Number of memoized values across all reactive scopes
88 */
89 memoValues: number;
90
91 /**
92 * The number of reactive scopes that were created but had to be discarded
93 * because they contained hook calls.
94 */
95 prunedMemoBlocks: number;
96
97 /**
98 * The total number of values that should have been memoized but weren't
99 * because they were part of a pruned memo block.
100 */
101 prunedMemoValues: number;
102
103 outlined: Array<{
104 fn: CodegenFunction;
105 type: ReactFunctionType | null;
106 }>;
107 };
108
109 export function codegenFunction(
110 fn: ReactiveFunction,
111 {
112 uniqueIdentifiers,
113 fbtOperands,
114 }: {
115 uniqueIdentifiers: Set<string>;
116 fbtOperands: Set<IdentifierId>;
117 },
118 ): CodegenFunction {
119 const cx = new Context(
120 fn.env,
121 fn.id ?? '[[ anonymous ]]',
122 uniqueIdentifiers,
123 fbtOperands,
124 null,
125 );
126
127 /**
128 * Fast Refresh reuses component instances at runtime even as the source of the component changes.
129 * The generated code needs to prevent values from one version of the code being reused after a code cange.
130 * If HMR detection is enabled and we know the source code of the component, assign a cache slot to track
131 * the source hash, and later, emit code to check for source changes and reset the cache on source changes.
132 */
133 let fastRefreshState: {
134 cacheIndex: number;
135 hash: string;
136 } | null = null;
137 if (
138 fn.env.config.enableResetCacheOnSourceFileChanges &&
139 fn.env.code !== null
140 ) {
141 const hash = createHmac('sha256', fn.env.code).digest('hex');
142 fastRefreshState = {
143 cacheIndex: cx.nextCacheIndex,
144 hash,
145 };
146 }
147
148 const compiled = codegenReactiveFunction(cx, fn);
149
150 const hookGuard = fn.env.config.enableEmitHookGuards;
151 if (hookGuard != null && fn.env.outputMode === 'client') {
152 compiled.body = t.blockStatement([
153 createHookGuard(
154 hookGuard,
155 fn.env.programContext,
156 compiled.body.body,
157 GuardKind.PushHookGuard,
158 GuardKind.PopHookGuard,
159 ),
160 ]);
161 }
162
163 const cacheCount = compiled.memoSlotsUsed;
164 if (cacheCount !== 0) {
165 const preface: Array<t.Statement> = [];
166 const useMemoCacheIdentifier =
167 fn.env.programContext.addMemoCacheImport().name;
168
169 // The import declaration for `useMemoCache` is inserted in the Babel plugin
170 preface.push(
171 t.variableDeclaration('const', [
172 t.variableDeclarator(
173 t.identifier(cx.synthesizeName('$')),
174 t.callExpression(t.identifier(useMemoCacheIdentifier), [
175 t.numericLiteral(cacheCount),
176 ]),
177 ),
178 ]),
179 );
180 if (fastRefreshState !== null) {
181 // HMR detection is enabled, emit code to reset the memo cache on source changes
182 const index = cx.synthesizeName('$i');
183 preface.push(
184 t.ifStatement(
185 t.binaryExpression(
186 '!==',
187 t.memberExpression(
188 t.identifier(cx.synthesizeName('$')),
189 t.numericLiteral(fastRefreshState.cacheIndex),
190 true,
191 ),
192 t.stringLiteral(fastRefreshState.hash),
193 ),
194 t.blockStatement([
195 t.forStatement(
196 t.variableDeclaration('let', [
197 t.variableDeclarator(t.identifier(index), t.numericLiteral(0)),
198 ]),
199 t.binaryExpression(
200 '<',
201 t.identifier(index),
202 t.numericLiteral(cacheCount),
203 ),
204 t.assignmentExpression(
205 '+=',
206 t.identifier(index),
207 t.numericLiteral(1),
208 ),
209 t.blockStatement([
210 t.expressionStatement(
211 t.assignmentExpression(
212 '=',
213 t.memberExpression(
214 t.identifier(cx.synthesizeName('$')),
215 t.identifier(index),
216 true,
217 ),
218 t.callExpression(
219 t.memberExpression(
220 t.identifier('Symbol'),
221 t.identifier('for'),
222 ),
223 [t.stringLiteral(MEMO_CACHE_SENTINEL)],
224 ),
225 ),
226 ),
227 ]),
228 ),
229 t.expressionStatement(
230 t.assignmentExpression(
231 '=',
232 t.memberExpression(
233 t.identifier(cx.synthesizeName('$')),
234 t.numericLiteral(fastRefreshState.cacheIndex),
235 true,
236 ),
237 t.stringLiteral(fastRefreshState.hash),
238 ),
239 ),
240 ]),
241 ),
242 );
243 }
244 compiled.body.body.unshift(...preface);
245 }
246
247 const emitInstrumentForget = fn.env.config.enableEmitInstrumentForget;
248 if (
249 emitInstrumentForget != null &&
250 fn.id != null &&
251 fn.env.outputMode === 'client'
252 ) {
253 /*
254 * Technically, this is a conditional hook call. However, we expect
255 * __DEV__ and gating identifier to be runtime constants
256 */
257 const gating =
258 emitInstrumentForget.gating != null
259 ? t.identifier(
260 fn.env.programContext.addImportSpecifier(
261 emitInstrumentForget.gating,
262 ).name,
263 )
264 : null;
265
266 const globalGating =
267 emitInstrumentForget.globalGating != null
268 ? t.identifier(emitInstrumentForget.globalGating)
269 : null;
270
271 if (emitInstrumentForget.globalGating != null) {
272 const assertResult = fn.env.programContext.assertGlobalBinding(
273 emitInstrumentForget.globalGating,
274 );
275 if (assertResult.isErr()) {
276 fn.env.recordErrors(assertResult.unwrapErr());
277 }
278 }
279
280 let ifTest: t.Expression;
281 if (gating != null && globalGating != null) {
282 ifTest = t.logicalExpression('&&', globalGating, gating);
283 } else if (gating != null) {
284 ifTest = gating;
285 } else {
286 CompilerError.invariant(globalGating != null, {
287 reason:
288 'Bad config not caught! Expected at least one of gating or globalGating',
289 loc: GeneratedSource,
290 });
291 ifTest = globalGating;
292 }
293
294 const instrumentFnIdentifier = fn.env.programContext.addImportSpecifier(
295 emitInstrumentForget.fn,
296 ).name;
297 const test: t.IfStatement = t.ifStatement(
298 ifTest,
299 t.expressionStatement(
300 t.callExpression(t.identifier(instrumentFnIdentifier), [
301 t.stringLiteral(fn.id),
302 t.stringLiteral(fn.env.filename ?? ''),
303 ]),
304 ),
305 );
306 compiled.body.body.unshift(test);
307 }
308
309 const outlined: CodegenFunction['outlined'] = [];
310 for (const {fn: outlinedFunction, type} of cx.env.getOutlinedFunctions()) {
311 const reactiveFunction = buildReactiveFunction(outlinedFunction);
312 pruneUnusedLabels(reactiveFunction);
313 pruneUnusedLValues(reactiveFunction);
314 pruneHoistedContexts(reactiveFunction);
315
316 const identifiers = renameVariables(reactiveFunction);
317 const codegen = codegenReactiveFunction(
318 new Context(
319 cx.env,
320 reactiveFunction.id ?? '[[ anonymous ]]',
321 identifiers,
322 cx.fbtOperands,
323 ),
324 reactiveFunction,
325 );
326 outlined.push({fn: codegen, type});
327 }
328 compiled.outlined = outlined;
329
330 return compiled;
331 }
332
333 function codegenReactiveFunction(
334 cx: Context,
335 fn: ReactiveFunction,
336 ): CodegenFunction {
337 for (const param of fn.params) {
338 const place = param.kind === 'Identifier' ? param : param.place;
339 cx.temp.set(place.identifier.declarationId, null);
340 cx.declare(place.identifier);
341 }
342
343 const params = fn.params.map(param => convertParameter(param));
344 const body: t.BlockStatement = codegenBlock(cx, fn.body);
345 body.directives = fn.directives.map(d => t.directive(t.directiveLiteral(d)));
346 const statements = body.body;
347 if (statements.length !== 0) {
348 const last = statements[statements.length - 1];
349 if (last.type === 'ReturnStatement' && last.argument == null) {
350 statements.pop();
351 }
352 }
353
354 const countMemoBlockVisitor = new CountMemoBlockVisitor(fn.env);
355 visitReactiveFunction(fn, countMemoBlockVisitor, undefined);
356
357 return {
358 type: 'CodegenFunction',
359 loc: fn.loc,
360 id: fn.id !== null ? t.identifier(fn.id) : null,
361 nameHint: fn.nameHint,
362 params,
363 body,
364 generator: fn.generator,
365 async: fn.async,
366 memoSlotsUsed: cx.nextCacheIndex,
367 memoBlocks: countMemoBlockVisitor.memoBlocks,
368 memoValues: countMemoBlockVisitor.memoValues,
369 prunedMemoBlocks: countMemoBlockVisitor.prunedMemoBlocks,
370 prunedMemoValues: countMemoBlockVisitor.prunedMemoValues,
371 outlined: [],
372 };
373 }
374
375 class CountMemoBlockVisitor extends ReactiveFunctionVisitor<void> {
376 env: Environment;
377 memoBlocks: number = 0;
378 memoValues: number = 0;
379 prunedMemoBlocks: number = 0;
380 prunedMemoValues: number = 0;
381
382 constructor(env: Environment) {
383 super();
384 this.env = env;
385 }
386
387 override visitScope(scopeBlock: ReactiveScopeBlock, state: void): void {
388 this.memoBlocks += 1;
389 this.memoValues += scopeBlock.scope.declarations.size;
390 this.traverseScope(scopeBlock, state);
391 }
392
393 override visitPrunedScope(
394 scopeBlock: PrunedReactiveScopeBlock,
395 state: void,
396 ): void {
397 this.prunedMemoBlocks += 1;
398 this.prunedMemoValues += scopeBlock.scope.declarations.size;
399 this.traversePrunedScope(scopeBlock, state);
400 }
401 }
402
403 function convertParameter(
404 param: Place | SpreadPattern,
405 ): t.Identifier | t.RestElement {
406 if (param.kind === 'Identifier') {
407 return convertIdentifier(param.identifier);
408 } else {
409 return t.restElement(convertIdentifier(param.place.identifier));
410 }
411 }
412
413 class Context {
414 env: Environment;
415 fnName: string;
416 #nextCacheIndex: number = 0;
417 /**
418 * Tracks which named variables have been declared to dedupe declarations,
419 * so this uses DeclarationId instead of IdentifierId
420 */
421 #declarations: Set<DeclarationId> = new Set();
422 temp: Temporaries;
423 objectMethods: Map<IdentifierId, ObjectMethod> = new Map();
424 uniqueIdentifiers: Set<string>;
425 fbtOperands: Set<IdentifierId>;
426 synthesizedNames: Map<string, ValidIdentifierName> = new Map();
427
428 constructor(
429 env: Environment,
430 fnName: string,
431 uniqueIdentifiers: Set<string>,
432 fbtOperands: Set<IdentifierId>,
433 temporaries: Temporaries | null = null,
434 ) {
435 this.env = env;
436 this.fnName = fnName;
437 this.uniqueIdentifiers = uniqueIdentifiers;
438 this.fbtOperands = fbtOperands;
439 this.temp = temporaries !== null ? new Map(temporaries) : new Map();
440 }
441
442 recordError(error: CompilerErrorDetail): void {
443 this.env.recordError(error);
444 }
445
446 get nextCacheIndex(): number {
447 return this.#nextCacheIndex++;
448 }
449
450 declare(identifier: Identifier): void {
451 this.#declarations.add(identifier.declarationId);
452 }
453
454 hasDeclared(identifier: Identifier): boolean {
455 return this.#declarations.has(identifier.declarationId);
456 }
457
458 synthesizeName(name: string): ValidIdentifierName {
459 const previous = this.synthesizedNames.get(name);
460 if (previous !== undefined) {
461 return previous;
462 }
463 let validated = makeIdentifierName(name).value;
464 let index = 0;
465 while (this.uniqueIdentifiers.has(validated)) {
466 validated = makeIdentifierName(`${name}${index++}`).value;
467 }
468 this.uniqueIdentifiers.add(validated);
469 this.synthesizedNames.set(name, validated);
470 return validated;
471 }
472 }
473
474 function codegenBlock(cx: Context, block: ReactiveBlock): t.BlockStatement {
475 const temp = new Map(cx.temp);
476 const result = codegenBlockNoReset(cx, block);
477 /*
478 * Check that the block only added new temporaries and did not update the
479 * value of any existing temporary
480 */
481 for (const [key, value] of cx.temp) {
482 if (!temp.has(key)) {
483 continue;
484 }
485 CompilerError.invariant(temp.get(key)! === value, {
486 reason: 'Expected temporary value to be unchanged',
487 loc: GeneratedSource,
488 });
489 }
490 cx.temp = temp;
491 return result;
492 }
493
494 /*
495 * Generates code for the block, without resetting the Context's temporary state.
496 * This should not be used unless it is expected that temporaries from this block
497 * can be referenced later, which is currently only true for sequence expressions
498 * where the final `value` is expected to reference the temporary created in the
499 * preceding instructions of the sequence.
500 */
501 function codegenBlockNoReset(
502 cx: Context,
503 block: ReactiveBlock,
504 ): t.BlockStatement {
505 const statements: Array<t.Statement> = [];
506 for (const item of block) {
507 switch (item.kind) {
508 case 'instruction': {
509 const statement = codegenInstructionNullable(cx, item.instruction);
510 if (statement !== null) {
511 statements.push(statement);
512 }
513 break;
514 }
515 case 'pruned-scope': {
516 const scopeBlock = codegenBlockNoReset(cx, item.instructions);
517 statements.push(...scopeBlock.body);
518 break;
519 }
520 case 'scope': {
521 const temp = new Map(cx.temp);
522 codegenReactiveScope(cx, statements, item.scope, item.instructions);
523 cx.temp = temp;
524 break;
525 }
526 case 'terminal': {
527 const statement = codegenTerminal(cx, item.terminal);
528 if (statement === null) {
529 break;
530 }
531 if (item.label !== null && !item.label.implicit) {
532 const block =
533 statement.type === 'BlockStatement' && statement.body.length === 1
534 ? statement.body[0]
535 : statement;
536 statements.push(
537 t.labeledStatement(
538 t.identifier(codegenLabel(item.label.id)),
539 block,
540 ),
541 );
542 } else if (statement.type === 'BlockStatement') {
543 statements.push(...statement.body);
544 } else {
545 statements.push(statement);
546 }
547 break;
548 }
549 default: {
550 assertExhaustive(
551 item,
552 `Unexpected item kind \`${(item as any).kind}\``,
553 );
554 }
555 }
556 }
557 return t.blockStatement(statements);
558 }
559
560 function codegenReactiveScope(
561 cx: Context,
562 statements: Array<t.Statement>,
563 scope: ReactiveScope,
564 block: ReactiveBlock,
565 ): void {
566 const cacheStoreStatements: Array<t.Statement> = [];
567 const cacheLoadStatements: Array<t.Statement> = [];
568 const cacheLoads: Array<{
569 name: t.Identifier;
570 index: number;
571 value: t.Expression;
572 }> = [];
573 const changeExpressions: Array<t.Expression> = [];
574
575 for (const dep of [...scope.dependencies].sort(compareScopeDependency)) {
576 const index = cx.nextCacheIndex;
577 const comparison = t.binaryExpression(
578 '!==',
579 t.memberExpression(
580 t.identifier(cx.synthesizeName('$')),
581 t.numericLiteral(index),
582 true,
583 ),
584 codegenDependency(cx, dep),
585 );
586 changeExpressions.push(comparison);
587 /*
588 * Adding directly to cacheStoreStatements rather than cacheLoads, because there
589 * is no corresponding cacheLoadStatement for dependencies
590 */
591 cacheStoreStatements.push(
592 t.expressionStatement(
593 t.assignmentExpression(
594 '=',
595 t.memberExpression(
596 t.identifier(cx.synthesizeName('$')),
597 t.numericLiteral(index),
598 true,
599 ),
600 codegenDependency(cx, dep),
601 ),
602 ),
603 );
604 }
605 let firstOutputIndex: number | null = null;
606
607 for (const [, {identifier}] of [...scope.declarations].sort(([, a], [, b]) =>
608 compareScopeDeclaration(a, b),
609 )) {
610 const index = cx.nextCacheIndex;
611 if (firstOutputIndex === null) {
612 firstOutputIndex = index;
613 }
614
615 CompilerError.invariant(identifier.name != null, {
616 reason: `Expected scope declaration identifier to be named`,
617 description: `Declaration \`${printIdentifier(
618 identifier,
619 )}\` is unnamed in scope @${scope.id}`,
620 loc: GeneratedSource,
621 });
622
623 const name = convertIdentifier(identifier);
624 if (!cx.hasDeclared(identifier)) {
625 statements.push(
626 t.variableDeclaration('let', [createVariableDeclarator(name, null)]),
627 );
628 }
629 cacheLoads.push({name, index, value: name});
630 cx.declare(identifier);
631 }
632 for (const reassignment of scope.reassignments) {
633 const index = cx.nextCacheIndex;
634 if (firstOutputIndex === null) {
635 firstOutputIndex = index;
636 }
637 const name = convertIdentifier(reassignment);
638 cacheLoads.push({name, index, value: name});
639 }
640
641 let testCondition = (changeExpressions as Array<t.Expression>).reduce(
642 (acc: t.Expression | null, ident: t.Expression) => {
643 if (acc == null) {
644 return ident;
645 }
646 return t.logicalExpression('||', acc, ident);
647 },
648 null as t.Expression | null,
649 );
650 if (testCondition === null) {
651 CompilerError.invariant(firstOutputIndex !== null, {
652 reason: `Expected scope to have at least one declaration`,
653 description: `Scope '@${scope.id}' has no declarations`,
654 loc: GeneratedSource,
655 });
656 testCondition = t.binaryExpression(
657 '===',
658 t.memberExpression(
659 t.identifier(cx.synthesizeName('$')),
660 t.numericLiteral(firstOutputIndex),
661 true,
662 ),
663 t.callExpression(
664 t.memberExpression(t.identifier('Symbol'), t.identifier('for')),
665 [t.stringLiteral(MEMO_CACHE_SENTINEL)],
666 ),
667 );
668 }
669
670 let computationBlock = codegenBlock(cx, block);
671
672 let memoStatement;
673 for (const {name, index, value} of cacheLoads) {
674 cacheStoreStatements.push(
675 t.expressionStatement(
676 t.assignmentExpression(
677 '=',
678 t.memberExpression(
679 t.identifier(cx.synthesizeName('$')),
680 t.numericLiteral(index),
681 true,
682 ),
683 value,
684 ),
685 ),
686 );
687 cacheLoadStatements.push(
688 t.expressionStatement(
689 t.assignmentExpression(
690 '=',
691 name,
692 t.memberExpression(
693 t.identifier(cx.synthesizeName('$')),
694 t.numericLiteral(index),
695 true,
696 ),
697 ),
698 ),
699 );
700 }
701 computationBlock.body.push(...cacheStoreStatements);
702 memoStatement = t.ifStatement(
703 testCondition,
704 computationBlock,
705 t.blockStatement(cacheLoadStatements),
706 );
707
708 statements.push(memoStatement);
709
710 const earlyReturnValue = scope.earlyReturnValue;
711 if (earlyReturnValue !== null) {
712 CompilerError.invariant(
713 earlyReturnValue.value.name !== null &&
714 earlyReturnValue.value.name.kind === 'named',
715 {
716 reason: `Expected early return value to be promoted to a named variable`,
717 loc: earlyReturnValue.loc,
718 },
719 );
720 const name: ValidIdentifierName = earlyReturnValue.value.name.value;
721 statements.push(
722 t.ifStatement(
723 t.binaryExpression(
724 '!==',
725 t.identifier(name),
726 t.callExpression(
727 t.memberExpression(t.identifier('Symbol'), t.identifier('for')),
728 [t.stringLiteral(EARLY_RETURN_SENTINEL)],
729 ),
730 ),
731 t.blockStatement([t.returnStatement(t.identifier(name))]),
732 ),
733 );
734 }
735 }
736
737 function codegenTerminal(
738 cx: Context,
739 terminal: ReactiveTerminal,
740 ): t.Statement | null {
741 switch (terminal.kind) {
742 case 'break': {
743 if (terminal.targetKind === 'implicit') {
744 return null;
745 }
746 return createBreakStatement(
747 terminal.loc,
748 terminal.targetKind === 'labeled'
749 ? t.identifier(codegenLabel(terminal.target))
750 : null,
751 );
752 }
753 case 'continue': {
754 if (terminal.targetKind === 'implicit') {
755 return null;
756 }
757 return createContinueStatement(
758 terminal.loc,
759 terminal.targetKind === 'labeled'
760 ? t.identifier(codegenLabel(terminal.target))
761 : null,
762 );
763 }
764 case 'for': {
765 return createForStatement(
766 terminal.loc,
767 codegenForInit(cx, terminal.init),
768 codegenInstructionValueToExpression(cx, terminal.test),
769 terminal.update !== null
770 ? codegenInstructionValueToExpression(cx, terminal.update)
771 : null,
772 codegenBlock(cx, terminal.loop),
773 );
774 }
775 case 'for-in': {
776 CompilerError.invariant(terminal.init.kind === 'SequenceExpression', {
777 reason: `Expected a sequence expression init for for..in`,
778 description: `Got \`${terminal.init.kind}\` expression instead`,
779 loc: terminal.init.loc,
780 });
781 if (terminal.init.instructions.length !== 2) {
782 cx.recordError(
783 new CompilerErrorDetail({
784 reason: 'Support non-trivial for..in inits',
785 category: ErrorCategory.Todo,
786 loc: terminal.init.loc,
787 suggestions: null,
788 }),
789 );
790 return t.emptyStatement();
791 }
792 const iterableCollection = terminal.init.instructions[0];
793 const iterableItem = terminal.init.instructions[1];
794 let lval: t.LVal;
795 switch (iterableItem.value.kind) {
796 case 'StoreLocal': {
797 lval = codegenLValue(cx, iterableItem.value.lvalue.place);
798 break;
799 }
800 case 'Destructure': {
801 lval = codegenLValue(cx, iterableItem.value.lvalue.pattern);
802 break;
803 }
804 case 'StoreContext': {
805 cx.recordError(
806 new CompilerErrorDetail({
807 reason: 'Support non-trivial for..in inits',
808 category: ErrorCategory.Todo,
809 loc: terminal.init.loc,
810 suggestions: null,
811 }),
812 );
813 return t.emptyStatement();
814 }
815 default:
816 CompilerError.invariant(false, {
817 reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
818 description: `Found ${iterableItem.value.kind}`,
819 loc: iterableItem.value.loc,
820 });
821 }
822 let varDeclKind: 'const' | 'let';
823 switch (iterableItem.value.lvalue.kind) {
824 case InstructionKind.Const:
825 varDeclKind = 'const' as const;
826 break;
827 case InstructionKind.Let:
828 varDeclKind = 'let' as const;
829 break;
830 case InstructionKind.Reassign:
831 CompilerError.invariant(false, {
832 reason:
833 'Destructure should never be Reassign as it would be an Object/ArrayPattern',
834 loc: iterableItem.loc,
835 });
836 case InstructionKind.Catch:
837 case InstructionKind.HoistedConst:
838 case InstructionKind.HoistedLet:
839 case InstructionKind.HoistedFunction:
840 case InstructionKind.Function:
841 CompilerError.invariant(false, {
842 reason: `Unexpected ${iterableItem.value.lvalue.kind} variable in for..in collection`,
843 loc: iterableItem.loc,
844 });
845 default:
846 assertExhaustive(
847 iterableItem.value.lvalue.kind,
848 `Unhandled lvalue kind: ${iterableItem.value.lvalue.kind}`,
849 );
850 }
851 return createForInStatement(
852 terminal.loc,
853 /*
854 * Special handling here since we only want the VariableDeclarators without any inits
855 * This needs to be updated when we handle non-trivial ForOf inits
856 */
857 createVariableDeclaration(iterableItem.value.loc, varDeclKind, [
858 t.variableDeclarator(lval, null),
859 ]),
860 codegenInstructionValueToExpression(cx, iterableCollection.value),
861 codegenBlock(cx, terminal.loop),
862 );
863 }
864 case 'for-of': {
865 CompilerError.invariant(
866 terminal.init.kind === 'SequenceExpression' &&
867 terminal.init.instructions.length === 1 &&
868 terminal.init.instructions[0].value.kind === 'GetIterator',
869 {
870 reason: `Expected a single-expression sequence expression init for for..of`,
871 description: `Got \`${terminal.init.kind}\` expression instead`,
872 loc: terminal.init.loc,
873 },
874 );
875 const iterableCollection = terminal.init.instructions[0].value;
876
877 CompilerError.invariant(terminal.test.kind === 'SequenceExpression', {
878 reason: `Expected a sequence expression test for for..of`,
879 description: `Got \`${terminal.init.kind}\` expression instead`,
880 loc: terminal.test.loc,
881 });
882 if (terminal.test.instructions.length !== 2) {
883 cx.recordError(
884 new CompilerErrorDetail({
885 reason: 'Support non-trivial for..of inits',
886 category: ErrorCategory.Todo,
887 loc: terminal.init.loc,
888 suggestions: null,
889 }),
890 );
891 return t.emptyStatement();
892 }
893 const iterableItem = terminal.test.instructions[1];
894 let lval: t.LVal;
895 switch (iterableItem.value.kind) {
896 case 'StoreLocal': {
897 lval = codegenLValue(cx, iterableItem.value.lvalue.place);
898 break;
899 }
900 case 'Destructure': {
901 lval = codegenLValue(cx, iterableItem.value.lvalue.pattern);
902 break;
903 }
904 case 'StoreContext': {
905 cx.recordError(
906 new CompilerErrorDetail({
907 reason: 'Support non-trivial for..of inits',
908 category: ErrorCategory.Todo,
909 loc: terminal.init.loc,
910 suggestions: null,
911 }),
912 );
913 return t.emptyStatement();
914 }
915 default:
916 CompilerError.invariant(false, {
917 reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
918 description: `Found ${iterableItem.value.kind}`,
919 loc: iterableItem.value.loc,
920 });
921 }
922 let varDeclKind: 'const' | 'let';
923 switch (iterableItem.value.lvalue.kind) {
924 case InstructionKind.Const:
925 varDeclKind = 'const' as const;
926 break;
927 case InstructionKind.Let:
928 varDeclKind = 'let' as const;
929 break;
930 case InstructionKind.Reassign:
931 case InstructionKind.Catch:
932 case InstructionKind.HoistedConst:
933 case InstructionKind.HoistedLet:
934 case InstructionKind.HoistedFunction:
935 case InstructionKind.Function:
936 CompilerError.invariant(false, {
937 reason: `Unexpected ${iterableItem.value.lvalue.kind} variable in for..of collection`,
938 loc: iterableItem.loc,
939 });
940 default:
941 assertExhaustive(
942 iterableItem.value.lvalue.kind,
943 `Unhandled lvalue kind: ${iterableItem.value.lvalue.kind}`,
944 );
945 }
946 return createForOfStatement(
947 terminal.loc,
948 /*
949 * Special handling here since we only want the VariableDeclarators without any inits
950 * This needs to be updated when we handle non-trivial ForOf inits
951 */
952 createVariableDeclaration(iterableItem.value.loc, varDeclKind, [
953 t.variableDeclarator(lval, null),
954 ]),
955 codegenInstructionValueToExpression(cx, iterableCollection),
956 codegenBlock(cx, terminal.loop),
957 );
958 }
959 case 'if': {
960 const test = codegenPlaceToExpression(cx, terminal.test);
961 const consequent = codegenBlock(cx, terminal.consequent);
962 let alternate: t.Statement | null = null;
963 if (terminal.alternate !== null) {
964 const block = codegenBlock(cx, terminal.alternate);
965 if (block.body.length !== 0) {
966 alternate = block;
967 }
968 }
969 return createIfStatement(terminal.loc, test, consequent, alternate);
970 }
971 case 'return': {
972 const value = codegenPlaceToExpression(cx, terminal.value);
973 if (value.type === 'Identifier' && value.name === 'undefined') {
974 // Use implicit undefined
975 return createReturnStatement(terminal.loc);
976 }
977 return createReturnStatement(terminal.loc, value);
978 }
979 case 'switch': {
980 return createSwitchStatement(
981 terminal.loc,
982 codegenPlaceToExpression(cx, terminal.test),
983 terminal.cases.map(case_ => {
984 const test =
985 case_.test !== null
986 ? codegenPlaceToExpression(cx, case_.test)
987 : null;
988 const block = codegenBlock(cx, case_.block!);
989 return t.switchCase(test, block.body.length === 0 ? [] : [block]);
990 }),
991 );
992 }
993 case 'throw': {
994 return createThrowStatement(
995 terminal.loc,
996 codegenPlaceToExpression(cx, terminal.value),
997 );
998 }
999 case 'do-while': {
1000 const test = codegenInstructionValueToExpression(cx, terminal.test);
1001 return createDoWhileStatement(
1002 terminal.loc,
1003 test,
1004 codegenBlock(cx, terminal.loop),
1005 );
1006 }
1007 case 'while': {
1008 const test = codegenInstructionValueToExpression(cx, terminal.test);
1009 return createWhileStatement(
1010 terminal.loc,
1011 test,
1012 codegenBlock(cx, terminal.loop),
1013 );
1014 }
1015 case 'label': {
1016 return codegenBlock(cx, terminal.block);
1017 }
1018 case 'try': {
1019 let catchParam = null;
1020 if (terminal.handlerBinding !== null) {
1021 catchParam = convertIdentifier(terminal.handlerBinding.identifier);
1022 cx.temp.set(terminal.handlerBinding.identifier.declarationId, null);
1023 }
1024 return createTryStatement(
1025 terminal.loc,
1026 codegenBlock(cx, terminal.block),
1027 t.catchClause(catchParam, codegenBlock(cx, terminal.handler)),
1028 );
1029 }
1030 default: {
1031 assertExhaustive(
1032 terminal,
1033 `Unexpected terminal kind \`${(terminal as any).kind}\``,
1034 );
1035 }
1036 }
1037 }
1038
1039 function codegenInstructionNullable(
1040 cx: Context,
1041 instr: ReactiveInstruction,
1042 ): t.Statement | null {
1043 if (
1044 instr.value.kind === 'StoreLocal' ||
1045 instr.value.kind === 'StoreContext' ||
1046 instr.value.kind === 'Destructure' ||
1047 instr.value.kind === 'DeclareLocal' ||
1048 instr.value.kind === 'DeclareContext'
1049 ) {
1050 let kind: InstructionKind = instr.value.lvalue.kind;
1051 let lvalue: Place | Pattern;
1052 let value: t.Expression | null;
1053 if (instr.value.kind === 'StoreLocal') {
1054 kind = cx.hasDeclared(instr.value.lvalue.place.identifier)
1055 ? InstructionKind.Reassign
1056 : kind;
1057 lvalue = instr.value.lvalue.place;
1058 value = codegenPlaceToExpression(cx, instr.value.value);
1059 } else if (instr.value.kind === 'StoreContext') {
1060 lvalue = instr.value.lvalue.place;
1061 value = codegenPlaceToExpression(cx, instr.value.value);
1062 } else if (
1063 instr.value.kind === 'DeclareLocal' ||
1064 instr.value.kind === 'DeclareContext'
1065 ) {
1066 if (cx.hasDeclared(instr.value.lvalue.place.identifier)) {
1067 return null;
1068 }
1069 kind = instr.value.lvalue.kind;
1070 lvalue = instr.value.lvalue.place;
1071 value = null;
1072 } else {
1073 lvalue = instr.value.lvalue.pattern;
1074 for (const place of eachPatternOperand(lvalue)) {
1075 if (
1076 kind !== InstructionKind.Reassign &&
1077 place.identifier.name === null
1078 ) {
1079 cx.temp.set(place.identifier.declarationId, null);
1080 }
1081 }
1082 value = codegenPlaceToExpression(cx, instr.value.value);
1083 }
1084 switch (kind) {
1085 case InstructionKind.Const: {
1086 CompilerError.invariant(instr.lvalue === null, {
1087 reason: `Const declaration cannot be referenced as an expression`,
1088 message: `this is ${kind}`,
1089 loc: instr.value.loc,
1090 });
1091 return createVariableDeclaration(instr.loc, 'const', [
1092 createVariableDeclarator(codegenLValue(cx, lvalue), value),
1093 ]);
1094 }
1095 case InstructionKind.Function: {
1096 CompilerError.invariant(instr.lvalue === null, {
1097 reason: `Function declaration cannot be referenced as an expression`,
1098 loc: instr.value.loc,
1099 });
1100 const genLvalue = codegenLValue(cx, lvalue);
1101 CompilerError.invariant(genLvalue.type === 'Identifier', {
1102 reason: 'Expected an identifier as a function declaration lvalue',
1103 loc: instr.value.loc,
1104 });
1105 CompilerError.invariant(value?.type === 'FunctionExpression', {
1106 reason: 'Expected a function as a function declaration value',
1107 description: `Got ${value == null ? String(value) : value.type} at ${printInstruction(instr)}`,
1108 loc: instr.value.loc,
1109 });
1110 return createFunctionDeclaration(
1111 instr.loc,
1112 genLvalue,
1113 value.params,
1114 value.body,
1115 value.generator,
1116 value.async,
1117 );
1118 }
1119 case InstructionKind.Let: {
1120 CompilerError.invariant(instr.lvalue === null, {
1121 reason: `Const declaration cannot be referenced as an expression`,
1122 message: `this is ${kind}`,
1123 loc: instr.value.loc,
1124 });
1125 return createVariableDeclaration(instr.loc, 'let', [
1126 createVariableDeclarator(codegenLValue(cx, lvalue), value),
1127 ]);
1128 }
1129 case InstructionKind.Reassign: {
1130 CompilerError.invariant(value !== null, {
1131 reason: 'Expected a value for reassignment',
1132 loc: instr.value.loc,
1133 });
1134 const expr = t.assignmentExpression(
1135 '=',
1136 codegenLValue(cx, lvalue),
1137 value,
1138 );
1139 if (instr.lvalue !== null) {
1140 if (instr.value.kind !== 'StoreContext') {
1141 cx.temp.set(instr.lvalue.identifier.declarationId, expr);
1142 return null;
1143 } else {
1144 // Handle chained reassignments for context variables
1145 const statement = codegenInstruction(cx, instr, expr);
1146 if (statement.type === 'EmptyStatement') {
1147 return null;
1148 }
1149 return statement;
1150 }
1151 } else {
1152 return createExpressionStatement(instr.loc, expr);
1153 }
1154 }
1155 case InstructionKind.Catch: {
1156 return t.emptyStatement();
1157 }
1158 case InstructionKind.HoistedLet:
1159 case InstructionKind.HoistedConst:
1160 case InstructionKind.HoistedFunction: {
1161 CompilerError.invariant(false, {
1162 reason: `Expected ${kind} to have been pruned in PruneHoistedContexts`,
1163 loc: instr.loc,
1164 });
1165 }
1166 default: {
1167 assertExhaustive(kind, `Unexpected instruction kind \`${kind}\``);
1168 }
1169 }
1170 } else if (
1171 instr.value.kind === 'StartMemoize' ||
1172 instr.value.kind === 'FinishMemoize'
1173 ) {
1174 return null;
1175 } else if (instr.value.kind === 'Debugger') {
1176 return t.debuggerStatement();
1177 } else if (instr.value.kind === 'ObjectMethod') {
1178 CompilerError.invariant(instr.lvalue, {
1179 reason: 'Expected object methods to have a temp lvalue',
1180 loc: GeneratedSource,
1181 });
1182 cx.objectMethods.set(instr.lvalue.identifier.id, instr.value);
1183 return null;
1184 } else {
1185 const value = codegenInstructionValue(cx, instr.value);
1186 const statement = codegenInstruction(cx, instr, value);
1187 if (statement.type === 'EmptyStatement') {
1188 return null;
1189 }
1190 return statement;
1191 }
1192 }
1193
1194 function codegenForInit(
1195 cx: Context,
1196 init: ReactiveValue,
1197 ): t.Expression | t.VariableDeclaration | null {
1198 if (init.kind === 'SequenceExpression') {
1199 const body = codegenBlock(
1200 cx,
1201 init.instructions.map(instruction => ({
1202 kind: 'instruction',
1203 instruction,
1204 })),
1205 ).body;
1206 const declarators: Array<t.VariableDeclarator> = [];
1207 let kind: 'let' | 'const' = 'const';
1208 body.forEach(instr => {
1209 let top: undefined | t.VariableDeclarator = undefined;
1210 if (
1211 instr.type === 'ExpressionStatement' &&
1212 instr.expression.type === 'AssignmentExpression' &&
1213 instr.expression.operator === '=' &&
1214 instr.expression.left.type === 'Identifier' &&
1215 (top = declarators.at(-1))?.id.type === 'Identifier' &&
1216 top?.id.name === instr.expression.left.name &&
1217 top?.init == null
1218 ) {
1219 top.init = instr.expression.right;
1220 } else {
1221 CompilerError.invariant(
1222 instr.type === 'VariableDeclaration' &&
1223 (instr.kind === 'let' || instr.kind === 'const'),
1224 {
1225 reason: 'Expected a variable declaration',
1226 description: `Got ${instr.type}`,
1227 loc: init.loc,
1228 },
1229 );
1230 if (instr.kind === 'let') {
1231 kind = 'let';
1232 }
1233 declarators.push(...instr.declarations);
1234 }
1235 });
1236 CompilerError.invariant(declarators.length > 0, {
1237 reason: 'Expected a variable declaration',
1238 loc: init.loc,
1239 });
1240 return t.variableDeclaration(kind, declarators);
1241 } else {
1242 return codegenInstructionValueToExpression(cx, init);
1243 }
1244 }
1245
1246 function codegenDependency(
1247 cx: Context,
1248 dependency: ReactiveScopeDependency,
1249 ): t.Expression {
1250 let object: t.Expression = convertIdentifier(dependency.identifier);
1251 if (dependency.path.length !== 0) {
1252 const hasOptional = dependency.path.some(path => path.optional);
1253 for (const path of dependency.path) {
1254 const property =
1255 typeof path.property === 'string'
1256 ? t.identifier(path.property)
1257 : t.numericLiteral(path.property);
1258 const isComputed = typeof path.property !== 'string';
1259 if (hasOptional) {
1260 object = t.optionalMemberExpression(
1261 object,
1262 property,
1263 isComputed,
1264 path.optional,
1265 );
1266 } else {
1267 object = t.memberExpression(object, property, isComputed);
1268 }
1269 }
1270 }
1271 return object;
1272 }
1273
1274 function withLoc<T extends (...args: Array<any>) => t.Node>(
1275 fn: T,
1276 ): (
1277 loc: SourceLocation | null | undefined,
1278 ...args: Parameters<T>
1279 ) => ReturnType<T> {
1280 return (
1281 loc: SourceLocation | null | undefined,
1282 ...args: Parameters<T>
1283 ): ReturnType<T> => {
1284 const node = fn(...args);
1285 if (loc != null && loc != GeneratedSource) {
1286 node.loc = loc;
1287 }
1288 return node as ReturnType<T>;
1289 };
1290 }
1291
1292 const createIdentifier = withLoc(t.identifier);
1293 const createArrayPattern = withLoc(t.arrayPattern);
1294 const createObjectPattern = withLoc(t.objectPattern);
1295 const createBinaryExpression = withLoc(t.binaryExpression);
1296 const createExpressionStatement = withLoc(t.expressionStatement);
1297 const _createLabelledStatement = withLoc(t.labeledStatement);
1298 const createVariableDeclaration = withLoc(t.variableDeclaration);
1299 const createFunctionDeclaration = withLoc(t.functionDeclaration);
1300 const createWhileStatement = withLoc(t.whileStatement);
1301 const createDoWhileStatement = withLoc(t.doWhileStatement);
1302 const createSwitchStatement = withLoc(t.switchStatement);
1303 const createIfStatement = withLoc(t.ifStatement);
1304 const createForStatement = withLoc(t.forStatement);
1305 const createForOfStatement = withLoc(t.forOfStatement);
1306 const createForInStatement = withLoc(t.forInStatement);
1307 const createTaggedTemplateExpression = withLoc(t.taggedTemplateExpression);
1308 const createLogicalExpression = withLoc(t.logicalExpression);
1309 const createSequenceExpression = withLoc(t.sequenceExpression);
1310 const createConditionalExpression = withLoc(t.conditionalExpression);
1311 const createTemplateLiteral = withLoc(t.templateLiteral);
1312 const createJsxNamespacedName = withLoc(t.jsxNamespacedName);
1313 const createJsxElement = withLoc(t.jsxElement);
1314 const createJsxAttribute = withLoc(t.jsxAttribute);
1315 const createJsxIdentifier = withLoc(t.jsxIdentifier);
1316 const createJsxExpressionContainer = withLoc(t.jsxExpressionContainer);
1317 const createJsxText = withLoc(t.jsxText);
1318 const createJsxClosingElement = withLoc(t.jsxClosingElement);
1319 const createJsxOpeningElement = withLoc(t.jsxOpeningElement);
1320 const createStringLiteral = withLoc(t.stringLiteral);
1321 const createThrowStatement = withLoc(t.throwStatement);
1322 const createTryStatement = withLoc(t.tryStatement);
1323 const createBreakStatement = withLoc(t.breakStatement);
1324 const createContinueStatement = withLoc(t.continueStatement);
1325 const createReturnStatement = withLoc(t.returnStatement);
1326
1327 function createVariableDeclarator(
1328 id: t.LVal,
1329 init?: t.Expression | null,
1330 ): t.VariableDeclarator {
1331 const node = t.variableDeclarator(id, init);
1332
1333 /*
1334 * The variable declarator location is not preserved in HIR, however, we can use the
1335 * start location of the id and the end location of the init to recreate the
1336 * exact original variable declarator location.
1337 *
1338 * Or if init is null, we likely have a declaration without an initializer, so we can use the id.loc.end as the end location.
1339 */
1340 if (id.loc && (init === null || init?.loc)) {
1341 node.loc = {
1342 start: id.loc.start,
1343 end: init?.loc?.end ?? id.loc.end,
1344 filename: id.loc.filename,
1345 identifierName: undefined,
1346 };
1347 }
1348
1349 return node;
1350 }
1351
1352 function createHookGuard(
1353 guard: ExternalFunction,
1354 context: ProgramContext,
1355 stmts: Array<t.Statement>,
1356 before: GuardKind,
1357 after: GuardKind,
1358 ): t.TryStatement {
1359 const guardFnName = context.addImportSpecifier(guard).name;
1360 function createHookGuardImpl(kind: number): t.ExpressionStatement {
1361 return t.expressionStatement(
1362 t.callExpression(t.identifier(guardFnName), [t.numericLiteral(kind)]),
1363 );
1364 }
1365
1366 return t.tryStatement(
1367 t.blockStatement([createHookGuardImpl(before), ...stmts]),
1368 null,
1369 t.blockStatement([createHookGuardImpl(after)]),
1370 );
1371 }
1372
1373 /**
1374 * Create a call expression.
1375 * If enableEmitHookGuards is set and the callExpression is a hook call,
1376 * the following transform will be made.
1377 * ```js
1378 * // source
1379 * useHook(arg1, arg2)
1380 *
1381 * // codegen
1382 * (() => {
1383 * try {
1384 * $dispatcherGuard(PUSH_EXPECT_HOOK);
1385 * return useHook(arg1, arg2);
1386 * } finally {
1387 * $dispatcherGuard(POP_EXPECT_HOOK);
1388 * }
1389 * })()
1390 * ```
1391 */
1392 function createCallExpression(
1393 env: Environment,
1394 callee: t.Expression,
1395 args: Array<t.Expression | t.SpreadElement>,
1396 loc: SourceLocation | null,
1397 isHook: boolean,
1398 ): t.CallExpression {
1399 const callExpr = t.callExpression(callee, args);
1400 if (loc != null && loc != GeneratedSource) {
1401 callExpr.loc = loc;
1402 }
1403
1404 const hookGuard = env.config.enableEmitHookGuards;
1405 if (hookGuard != null && isHook && env.outputMode === 'client') {
1406 const iife = t.functionExpression(
1407 null,
1408 [],
1409 t.blockStatement([
1410 createHookGuard(
1411 hookGuard,
1412 env.programContext,
1413 [t.returnStatement(callExpr)],
1414 GuardKind.AllowHook,
1415 GuardKind.DisallowHook,
1416 ),
1417 ]),
1418 );
1419 return t.callExpression(iife, []);
1420 } else {
1421 return callExpr;
1422 }
1423 }
1424
1425 type Temporaries = Map<DeclarationId, t.Expression | t.JSXText | null>;
1426
1427 function codegenLabel(id: BlockId): string {
1428 return `bb${id}`;
1429 }
1430
1431 function codegenInstruction(
1432 cx: Context,
1433 instr: ReactiveInstruction,
1434 value: t.Expression | t.JSXText,
1435 ): t.Statement {
1436 if (t.isStatement(value)) {
1437 return value;
1438 }
1439 if (instr.lvalue === null) {
1440 return t.expressionStatement(convertValueToExpression(value));
1441 }
1442 if (instr.lvalue.identifier.name === null) {
1443 // temporary
1444 cx.temp.set(instr.lvalue.identifier.declarationId, value);
1445 return t.emptyStatement();
1446 } else {
1447 const expressionValue = convertValueToExpression(value);
1448 if (cx.hasDeclared(instr.lvalue.identifier)) {
1449 return createExpressionStatement(
1450 instr.loc,
1451 t.assignmentExpression(
1452 '=',
1453 convertIdentifier(instr.lvalue.identifier),
1454 expressionValue,
1455 ),
1456 );
1457 } else {
1458 return createVariableDeclaration(instr.loc, 'const', [
1459 createVariableDeclarator(
1460 convertIdentifier(instr.lvalue.identifier),
1461 expressionValue,
1462 ),
1463 ]);
1464 }
1465 }
1466 }
1467
1468 function convertValueToExpression(
1469 value: t.JSXText | t.Expression,
1470 ): t.Expression {
1471 if (value.type === 'JSXText') {
1472 return createStringLiteral(value.loc, value.value);
1473 }
1474 return value;
1475 }
1476
1477 function codegenInstructionValueToExpression(
1478 cx: Context,
1479 instrValue: ReactiveValue,
1480 ): t.Expression {
1481 const value = codegenInstructionValue(cx, instrValue);
1482 return convertValueToExpression(value);
1483 }
1484
1485 function codegenInstructionValue(
1486 cx: Context,
1487 instrValue: ReactiveValue,
1488 ): t.Expression | t.JSXText {
1489 let value: t.Expression | t.JSXText;
1490 switch (instrValue.kind) {
1491 case 'ArrayExpression': {
1492 const elements = instrValue.elements.map(element => {
1493 if (element.kind === 'Identifier') {
1494 return codegenPlaceToExpression(cx, element);
1495 } else if (element.kind === 'Spread') {
1496 return t.spreadElement(codegenPlaceToExpression(cx, element.place));
1497 } else {
1498 return null;
1499 }
1500 });
1501 value = t.arrayExpression(elements);
1502 break;
1503 }
1504 case 'BinaryExpression': {
1505 const left = codegenPlaceToExpression(cx, instrValue.left);
1506 const right = codegenPlaceToExpression(cx, instrValue.right);
1507 value = createBinaryExpression(
1508 instrValue.loc,
1509 instrValue.operator,
1510 left,
1511 right,
1512 );
1513 break;
1514 }
1515 case 'UnaryExpression': {
1516 value = t.unaryExpression(
1517 instrValue.operator,
1518 codegenPlaceToExpression(cx, instrValue.value),
1519 );
1520 break;
1521 }
1522 case 'Primitive': {
1523 value = codegenValue(cx, instrValue.loc, instrValue.value);
1524 break;
1525 }
1526 case 'CallExpression': {
1527 if (cx.env.config.enableForest) {
1528 const callee = codegenPlaceToExpression(cx, instrValue.callee);
1529 const args = instrValue.args.map(arg => codegenArgument(cx, arg));
1530 value = t.callExpression(callee, args);
1531 if (instrValue.typeArguments != null) {
1532 value.typeArguments = t.typeParameterInstantiation(
1533 instrValue.typeArguments,
1534 );
1535 }
1536 break;
1537 }
1538 const isHook = getHookKind(cx.env, instrValue.callee.identifier) != null;
1539 const callee = codegenPlaceToExpression(cx, instrValue.callee);
1540 const args = instrValue.args.map(arg => codegenArgument(cx, arg));
1541 value = createCallExpression(
1542 cx.env,
1543 callee,
1544 args,
1545 instrValue.loc,
1546 isHook,
1547 );
1548 break;
1549 }
1550 case 'OptionalExpression': {
1551 const optionalValue = codegenInstructionValueToExpression(
1552 cx,
1553 instrValue.value,
1554 );
1555 switch (optionalValue.type) {
1556 case 'OptionalCallExpression':
1557 case 'CallExpression': {
1558 CompilerError.invariant(t.isExpression(optionalValue.callee), {
1559 reason: 'v8 intrinsics are validated during lowering',
1560 loc: optionalValue.callee.loc ?? GeneratedSource,
1561 });
1562 value = t.optionalCallExpression(
1563 optionalValue.callee,
1564 optionalValue.arguments,
1565 instrValue.optional,
1566 );
1567 break;
1568 }
1569 case 'OptionalMemberExpression':
1570 case 'MemberExpression': {
1571 const property = optionalValue.property;
1572 CompilerError.invariant(t.isExpression(property), {
1573 reason: 'Private names are validated during lowering',
1574 loc: property.loc ?? GeneratedSource,
1575 });
1576 value = t.optionalMemberExpression(
1577 optionalValue.object,
1578 property,
1579 optionalValue.computed,
1580 instrValue.optional,
1581 );
1582 break;
1583 }
1584 default: {
1585 CompilerError.invariant(false, {
1586 reason:
1587 'Expected an optional value to resolve to a call expression or member expression',
1588 description: `Got a \`${optionalValue.type}\``,
1589 loc: instrValue.loc,
1590 });
1591 }
1592 }
1593 break;
1594 }
1595 case 'MethodCall': {
1596 const isHook =
1597 getHookKind(cx.env, instrValue.property.identifier) != null;
1598 const memberExpr = codegenPlaceToExpression(cx, instrValue.property);
1599 CompilerError.invariant(
1600 t.isMemberExpression(memberExpr) ||
1601 t.isOptionalMemberExpression(memberExpr),
1602 {
1603 reason:
1604 '[Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression',
1605 message: `Got: '${memberExpr.type}'`,
1606 loc: memberExpr.loc ?? GeneratedSource,
1607 },
1608 );
1609 CompilerError.invariant(
1610 t.isNodesEquivalent(
1611 memberExpr.object,
1612 codegenPlaceToExpression(cx, instrValue.receiver),
1613 ),
1614 {
1615 reason:
1616 '[Codegen] Internal error: Forget should always generate MethodCall::property ' +
1617 'as a MemberExpression of MethodCall::receiver',
1618 loc: memberExpr.loc ?? GeneratedSource,
1619 },
1620 );
1621 const args = instrValue.args.map(arg => codegenArgument(cx, arg));
1622 value = createCallExpression(
1623 cx.env,
1624 memberExpr,
1625 args,
1626 instrValue.loc,
1627 isHook,
1628 );
1629 break;
1630 }
1631 case 'NewExpression': {
1632 const callee = codegenPlaceToExpression(cx, instrValue.callee);
1633 const args = instrValue.args.map(arg => codegenArgument(cx, arg));
1634 value = t.newExpression(callee, args);
1635 break;
1636 }
1637 case 'ObjectExpression': {
1638 const properties = [];
1639 for (const property of instrValue.properties) {
1640 if (property.kind === 'ObjectProperty') {
1641 const key = codegenObjectPropertyKey(cx, property.key);
1642
1643 switch (property.type) {
1644 case 'property': {
1645 const value = codegenPlaceToExpression(cx, property.place);
1646 properties.push(
1647 t.objectProperty(
1648 key,
1649 value,
1650 property.key.kind === 'computed',
1651 key.type === 'Identifier' &&
1652 value.type === 'Identifier' &&
1653 value.name === key.name,
1654 ),
1655 );
1656 break;
1657 }
1658 case 'method': {
1659 const method = cx.objectMethods.get(property.place.identifier.id);
1660 CompilerError.invariant(method, {
1661 reason: 'Expected ObjectMethod instruction',
1662 loc: GeneratedSource,
1663 });
1664 const loweredFunc = method.loweredFunc;
1665 const reactiveFunction = buildReactiveFunction(loweredFunc.func);
1666 pruneUnusedLabels(reactiveFunction);
1667 pruneUnusedLValues(reactiveFunction);
1668 const fn = codegenReactiveFunction(
1669 new Context(
1670 cx.env,
1671 reactiveFunction.id ?? '[[ anonymous ]]',
1672 cx.uniqueIdentifiers,
1673 cx.fbtOperands,
1674 cx.temp,
1675 ),
1676 reactiveFunction,
1677 );
1678
1679 /*
1680 * ObjectMethod builder must be backwards compatible with older versions of babel.
1681 * https://github.com/babel/babel/blob/v7.7.4/packages/babel-types/src/definitions/core.js#L599-L603
1682 */
1683 const babelNode = t.objectMethod(
1684 'method',
1685 key,
1686 fn.params,
1687 fn.body,
1688 false,
1689 );
1690 babelNode.async = fn.async;
1691 babelNode.generator = fn.generator;
1692 properties.push(babelNode);
1693 break;
1694 }
1695 default:
1696 assertExhaustive(
1697 property.type,
1698 `Unexpected property type: ${property.type}`,
1699 );
1700 }
1701 } else {
1702 properties.push(
1703 t.spreadElement(codegenPlaceToExpression(cx, property.place)),
1704 );
1705 }
1706 }
1707 value = t.objectExpression(properties);
1708 break;
1709 }
1710 case 'JSXText': {
1711 value = createJsxText(instrValue.loc, instrValue.value);
1712 break;
1713 }
1714 case 'JsxExpression': {
1715 const attributes: Array<t.JSXAttribute | t.JSXSpreadAttribute> = [];
1716 for (const attribute of instrValue.props) {
1717 attributes.push(codegenJsxAttribute(cx, attribute));
1718 }
1719 let tagValue =
1720 instrValue.tag.kind === 'Identifier'
1721 ? codegenPlaceToExpression(cx, instrValue.tag)
1722 : t.stringLiteral(instrValue.tag.name);
1723 let tag: t.JSXIdentifier | t.JSXNamespacedName | t.JSXMemberExpression;
1724 if (tagValue.type === 'Identifier') {
1725 tag = createJsxIdentifier(instrValue.tag.loc, tagValue.name);
1726 } else if (tagValue.type === 'MemberExpression') {
1727 tag = convertMemberExpressionToJsx(tagValue);
1728 } else {
1729 CompilerError.invariant(tagValue.type === 'StringLiteral', {
1730 reason: `Expected JSX tag to be an identifier or string, got \`${tagValue.type}\``,
1731 loc: tagValue.loc ?? GeneratedSource,
1732 });
1733 if (tagValue.value.indexOf(':') >= 0) {
1734 const [namespace, name] = tagValue.value.split(':', 2);
1735 tag = createJsxNamespacedName(
1736 instrValue.tag.loc,
1737 createJsxIdentifier(instrValue.tag.loc, namespace),
1738 createJsxIdentifier(instrValue.tag.loc, name),
1739 );
1740 } else {
1741 tag = createJsxIdentifier(instrValue.loc, tagValue.value);
1742 }
1743 }
1744 let children;
1745 if (
1746 tagValue.type === 'StringLiteral' &&
1747 SINGLE_CHILD_FBT_TAGS.has(tagValue.value)
1748 ) {
1749 CompilerError.invariant(instrValue.children != null, {
1750 reason: 'Expected fbt element to have children',
1751 loc: instrValue.loc,
1752 });
1753 children = instrValue.children.map(child =>
1754 codegenJsxFbtChildElement(cx, child),
1755 );
1756 } else {
1757 children =
1758 instrValue.children !== null
1759 ? instrValue.children.map(child => codegenJsxElement(cx, child))
1760 : [];
1761 }
1762 value = createJsxElement(
1763 instrValue.loc,
1764 createJsxOpeningElement(
1765 instrValue.openingLoc,
1766 tag,
1767 attributes,
1768 instrValue.children === null,
1769 ),
1770 instrValue.children !== null
1771 ? createJsxClosingElement(instrValue.closingLoc, tag)
1772 : null,
1773 children,
1774 instrValue.children === null,
1775 );
1776 break;
1777 }
1778 case 'JsxFragment': {
1779 value = t.jsxFragment(
1780 t.jsxOpeningFragment(),
1781 t.jsxClosingFragment(),
1782 instrValue.children.map(child => codegenJsxElement(cx, child)),
1783 );
1784 break;
1785 }
1786 case 'UnsupportedNode': {
1787 const node = instrValue.node;
1788 if (!t.isExpression(node)) {
1789 return node as any; // TODO handle statements, jsx fragments
1790 }
1791 value = node;
1792 break;
1793 }
1794 case 'PropertyStore':
1795 case 'PropertyLoad':
1796 case 'PropertyDelete': {
1797 let memberExpr;
1798 /*
1799 * We currently only lower single chains of optional memberexpr.
1800 * (See BuildHIR.ts for more detail.)
1801 */
1802 if (typeof instrValue.property === 'string') {
1803 memberExpr = t.memberExpression(
1804 codegenPlaceToExpression(cx, instrValue.object),
1805 t.identifier(instrValue.property),
1806 );
1807 } else {
1808 memberExpr = t.memberExpression(
1809 codegenPlaceToExpression(cx, instrValue.object),
1810 t.numericLiteral(instrValue.property),
1811 true,
1812 );
1813 }
1814 if (instrValue.kind === 'PropertyStore') {
1815 value = t.assignmentExpression(
1816 '=',
1817 memberExpr,
1818 codegenPlaceToExpression(cx, instrValue.value),
1819 );
1820 } else if (instrValue.kind === 'PropertyLoad') {
1821 value = memberExpr;
1822 } else {
1823 value = t.unaryExpression('delete', memberExpr);
1824 }
1825 break;
1826 }
1827 case 'ComputedStore': {
1828 value = t.assignmentExpression(
1829 '=',
1830 t.memberExpression(
1831 codegenPlaceToExpression(cx, instrValue.object),
1832 codegenPlaceToExpression(cx, instrValue.property),
1833 true,
1834 ),
1835 codegenPlaceToExpression(cx, instrValue.value),
1836 );
1837 break;
1838 }
1839 case 'ComputedLoad': {
1840 const object = codegenPlaceToExpression(cx, instrValue.object);
1841 const property = codegenPlaceToExpression(cx, instrValue.property);
1842 value = t.memberExpression(object, property, true);
1843 break;
1844 }
1845 case 'ComputedDelete': {
1846 value = t.unaryExpression(
1847 'delete',
1848 t.memberExpression(
1849 codegenPlaceToExpression(cx, instrValue.object),
1850 codegenPlaceToExpression(cx, instrValue.property),
1851 true,
1852 ),
1853 );
1854 break;
1855 }
1856 case 'LoadLocal':
1857 case 'LoadContext': {
1858 value = codegenPlaceToExpression(cx, instrValue.place);
1859 break;
1860 }
1861 case 'FunctionExpression': {
1862 const loweredFunc = instrValue.loweredFunc.func;
1863 const reactiveFunction = buildReactiveFunction(loweredFunc);
1864 pruneUnusedLabels(reactiveFunction);
1865 pruneUnusedLValues(reactiveFunction);
1866 pruneHoistedContexts(reactiveFunction);
1867 const fn = codegenReactiveFunction(
1868 new Context(
1869 cx.env,
1870 reactiveFunction.id ?? '[[ anonymous ]]',
1871 cx.uniqueIdentifiers,
1872 cx.fbtOperands,
1873 cx.temp,
1874 ),
1875 reactiveFunction,
1876 );
1877
1878 if (instrValue.type === 'ArrowFunctionExpression') {
1879 let body: t.BlockStatement | t.Expression = fn.body;
1880 if (body.body.length === 1 && loweredFunc.directives.length == 0) {
1881 const stmt = body.body[0]!;
1882 if (stmt.type === 'ReturnStatement' && stmt.argument != null) {
1883 body = stmt.argument;
1884 }
1885 }
1886 value = t.arrowFunctionExpression(fn.params, body, fn.async);
1887 } else {
1888 value = t.functionExpression(
1889 instrValue.name != null ? t.identifier(instrValue.name) : null,
1890 fn.params,
1891 fn.body,
1892 fn.generator,
1893 fn.async,
1894 );
1895 }
1896 if (
1897 cx.env.config.enableNameAnonymousFunctions &&
1898 instrValue.name == null &&
1899 instrValue.nameHint != null
1900 ) {
1901 const name = instrValue.nameHint;
1902 value = t.memberExpression(
1903 t.objectExpression([t.objectProperty(t.stringLiteral(name), value)]),
1904 t.stringLiteral(name),
1905 true,
1906 false,
1907 );
1908 }
1909 break;
1910 }
1911 case 'TaggedTemplateExpression': {
1912 value = createTaggedTemplateExpression(
1913 instrValue.loc,
1914 codegenPlaceToExpression(cx, instrValue.tag),
1915 t.templateLiteral([t.templateElement(instrValue.value)], []),
1916 );
1917 break;
1918 }
1919 case 'TypeCastExpression': {
1920 if (t.isTSType(instrValue.typeAnnotation)) {
1921 if (instrValue.typeAnnotationKind === 'satisfies') {
1922 value = t.tsSatisfiesExpression(
1923 codegenPlaceToExpression(cx, instrValue.value),
1924 instrValue.typeAnnotation,
1925 );
1926 } else {
1927 value = t.tsAsExpression(
1928 codegenPlaceToExpression(cx, instrValue.value),
1929 instrValue.typeAnnotation,
1930 );
1931 }
1932 } else {
1933 value = t.typeCastExpression(
1934 codegenPlaceToExpression(cx, instrValue.value),
1935 t.typeAnnotation(instrValue.typeAnnotation),
1936 );
1937 }
1938 break;
1939 }
1940 case 'LogicalExpression': {
1941 value = createLogicalExpression(
1942 instrValue.loc,
1943 instrValue.operator,
1944 codegenInstructionValueToExpression(cx, instrValue.left),
1945 codegenInstructionValueToExpression(cx, instrValue.right),
1946 );
1947 break;
1948 }
1949 case 'ConditionalExpression': {
1950 value = createConditionalExpression(
1951 instrValue.loc,
1952 codegenInstructionValueToExpression(cx, instrValue.test),
1953 codegenInstructionValueToExpression(cx, instrValue.consequent),
1954 codegenInstructionValueToExpression(cx, instrValue.alternate),
1955 );
1956 break;
1957 }
1958 case 'SequenceExpression': {
1959 const body = codegenBlockNoReset(
1960 cx,
1961 instrValue.instructions.map(instruction => ({
1962 kind: 'instruction',
1963 instruction,
1964 })),
1965 ).body;
1966 const expressions = body.map(stmt => {
1967 if (stmt.type === 'ExpressionStatement') {
1968 return stmt.expression;
1969 } else {
1970 if (t.isVariableDeclaration(stmt)) {
1971 const declarator = stmt.declarations[0];
1972 cx.recordError(
1973 new CompilerErrorDetail({
1974 reason: `(CodegenReactiveFunction::codegenInstructionValue) Cannot declare variables in a value block, tried to declare '${
1975 (declarator.id as t.Identifier).name
1976 }'`,
1977 category: ErrorCategory.Todo,
1978 loc: declarator.loc ?? null,
1979 suggestions: null,
1980 }),
1981 );
1982 return t.stringLiteral(`TODO handle ${declarator.id}`);
1983 } else {
1984 cx.recordError(
1985 new CompilerErrorDetail({
1986 reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`,
1987 category: ErrorCategory.Todo,
1988 loc: stmt.loc ?? null,
1989 suggestions: null,
1990 }),
1991 );
1992 return t.stringLiteral(`TODO handle ${stmt.type}`);
1993 }
1994 }
1995 });
1996 if (expressions.length === 0) {
1997 value = codegenInstructionValueToExpression(cx, instrValue.value);
1998 } else {
1999 value = createSequenceExpression(instrValue.loc, [
2000 ...expressions,
2001 codegenInstructionValueToExpression(cx, instrValue.value),
2002 ]);
2003 }
2004 break;
2005 }
2006 case 'TemplateLiteral': {
2007 value = createTemplateLiteral(
2008 instrValue.loc,
2009 instrValue.quasis.map(q => t.templateElement(q)),
2010 instrValue.subexprs.map(p => codegenPlaceToExpression(cx, p)),
2011 );
2012 break;
2013 }
2014 case 'LoadGlobal': {
2015 value = t.identifier(instrValue.binding.name);
2016 break;
2017 }
2018 case 'RegExpLiteral': {
2019 value = t.regExpLiteral(instrValue.pattern, instrValue.flags);
2020 break;
2021 }
2022 case 'MetaProperty': {
2023 value = t.metaProperty(
2024 t.identifier(instrValue.meta),
2025 t.identifier(instrValue.property),
2026 );
2027 break;
2028 }
2029 case 'Await': {
2030 value = t.awaitExpression(codegenPlaceToExpression(cx, instrValue.value));
2031 break;
2032 }
2033 case 'GetIterator': {
2034 value = codegenPlaceToExpression(cx, instrValue.collection);
2035 break;
2036 }
2037 case 'IteratorNext': {
2038 value = codegenPlaceToExpression(cx, instrValue.iterator);
2039 break;
2040 }
2041 case 'NextPropertyOf': {
2042 value = codegenPlaceToExpression(cx, instrValue.value);
2043 break;
2044 }
2045 case 'PostfixUpdate': {
2046 value = t.updateExpression(
2047 instrValue.operation,
2048 codegenPlaceToExpression(cx, instrValue.lvalue),
2049 false,
2050 );
2051 break;
2052 }
2053 case 'PrefixUpdate': {
2054 value = t.updateExpression(
2055 instrValue.operation,
2056 codegenPlaceToExpression(cx, instrValue.lvalue),
2057 true,
2058 );
2059 break;
2060 }
2061 case 'StoreLocal': {
2062 CompilerError.invariant(
2063 instrValue.lvalue.kind === InstructionKind.Reassign,
2064 {
2065 reason: `Unexpected StoreLocal in codegenInstructionValue`,
2066 loc: instrValue.loc,
2067 },
2068 );
2069 value = t.assignmentExpression(
2070 '=',
2071 codegenLValue(cx, instrValue.lvalue.place),
2072 codegenPlaceToExpression(cx, instrValue.value),
2073 );
2074 break;
2075 }
2076 case 'StoreGlobal': {
2077 value = t.assignmentExpression(
2078 '=',
2079 t.identifier(instrValue.name),
2080 codegenPlaceToExpression(cx, instrValue.value),
2081 );
2082 break;
2083 }
2084 case 'StartMemoize':
2085 case 'FinishMemoize':
2086 case 'Debugger':
2087 case 'DeclareLocal':
2088 case 'DeclareContext':
2089 case 'Destructure':
2090 case 'ObjectMethod':
2091 case 'StoreContext': {
2092 CompilerError.invariant(false, {
2093 reason: `Unexpected ${instrValue.kind} in codegenInstructionValue`,
2094 loc: instrValue.loc,
2095 });
2096 }
2097 default: {
2098 assertExhaustive(
2099 instrValue,
2100 `Unexpected instruction value kind \`${(instrValue as any).kind}\``,
2101 );
2102 }
2103 }
2104 if (instrValue.loc != null && instrValue.loc != GeneratedSource) {
2105 value.loc = instrValue.loc;
2106 }
2107 return value;
2108 }
2109
2110 /**
2111 * Due to a bug in earlier Babel versions, JSX string attributes with double quotes, unicode characters, or special
2112 * control characters such as \n may be escaped unnecessarily. To avoid trigger this Babel bug, we use a
2113 * JsxExpressionContainer for such strings.
2114 *
2115 * u0000 to u001F: C0 control codes
2116 * u007F : Delete character
2117 * u0080 to u009F: C1 control codes
2118 * u00A0 to uFFFF: All non-basic Latin characters
2119 * https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes
2120 *
2121 * u010000 to u10FFFF: Astral plane characters
2122 * https://mathiasbynens.be/notes/javascript-unicode
2123 */
2124 const STRING_REQUIRES_EXPR_CONTAINER_PATTERN =
2125 /[\u{0000}-\u{001F}\u{007F}\u{0080}-\u{FFFF}\u{010000}-\u{10FFFF}]|"|\\/u;
2126 function codegenJsxAttribute(
2127 cx: Context,
2128 attribute: JsxAttribute,
2129 ): t.JSXAttribute | t.JSXSpreadAttribute {
2130 switch (attribute.kind) {
2131 case 'JsxAttribute': {
2132 let propName: t.JSXIdentifier | t.JSXNamespacedName;
2133 if (attribute.name.indexOf(':') === -1) {
2134 propName = createJsxIdentifier(attribute.place.loc, attribute.name);
2135 } else {
2136 const [namespace, name] = attribute.name.split(':', 2);
2137 propName = createJsxNamespacedName(
2138 attribute.place.loc,
2139 createJsxIdentifier(attribute.place.loc, namespace),
2140 createJsxIdentifier(attribute.place.loc, name),
2141 );
2142 }
2143 const innerValue = codegenPlaceToExpression(cx, attribute.place);
2144 let value;
2145 switch (innerValue.type) {
2146 case 'StringLiteral': {
2147 value = innerValue;
2148 if (
2149 STRING_REQUIRES_EXPR_CONTAINER_PATTERN.test(value.value) &&
2150 !cx.fbtOperands.has(attribute.place.identifier.id)
2151 ) {
2152 value = createJsxExpressionContainer(value.loc, value);
2153 }
2154 break;
2155 }
2156 default: {
2157 /*
2158 * NOTE JSXFragment is technically allowed as an attribute value per the spec
2159 * but many tools do not support this case. We emit fragments wrapped in an
2160 * expression container for compatibility purposes.
2161 * spec: https://github.com/facebook/jsx/blob/main/AST.md#jsx-attributes
2162 */
2163 value = createJsxExpressionContainer(attribute.place.loc, innerValue);
2164 break;
2165 }
2166 }
2167 return createJsxAttribute(attribute.place.loc, propName, value);
2168 }
2169 case 'JsxSpreadAttribute': {
2170 return t.jsxSpreadAttribute(
2171 codegenPlaceToExpression(cx, attribute.argument),
2172 );
2173 }
2174 default: {
2175 assertExhaustive(
2176 attribute,
2177 `Unexpected attribute kind \`${(attribute as any).kind}\``,
2178 );
2179 }
2180 }
2181 }
2182
2183 const JSX_TEXT_CHILD_REQUIRES_EXPR_CONTAINER_PATTERN = /[<>&{}]/;
2184 function codegenJsxElement(
2185 cx: Context,
2186 place: Place,
2187 ):
2188 | t.JSXText
2189 | t.JSXExpressionContainer
2190 | t.JSXSpreadChild
2191 | t.JSXElement
2192 | t.JSXFragment {
2193 const value = codegenPlace(cx, place);
2194 switch (value.type) {
2195 case 'JSXText': {
2196 if (JSX_TEXT_CHILD_REQUIRES_EXPR_CONTAINER_PATTERN.test(value.value)) {
2197 return createJsxExpressionContainer(
2198 place.loc,
2199 createStringLiteral(place.loc, value.value),
2200 );
2201 }
2202 return createJsxText(place.loc, value.value);
2203 }
2204 case 'JSXElement':
2205 case 'JSXFragment': {
2206 return value;
2207 }
2208 default: {
2209 return createJsxExpressionContainer(place.loc, value);
2210 }
2211 }
2212 }
2213
2214 function codegenJsxFbtChildElement(
2215 cx: Context,
2216 place: Place,
2217 ):
2218 | t.JSXText
2219 | t.JSXExpressionContainer
2220 | t.JSXSpreadChild
2221 | t.JSXElement
2222 | t.JSXFragment {
2223 const value = codegenPlace(cx, place);
2224 switch (value.type) {
2225 // fbt:param only allows JSX element or expression container as children
2226 case 'JSXText':
2227 case 'JSXElement': {
2228 return value;
2229 }
2230 default: {
2231 return createJsxExpressionContainer(place.loc, value);
2232 }
2233 }
2234 }
2235
2236 function convertMemberExpressionToJsx(
2237 expr: t.MemberExpression,
2238 ): t.JSXMemberExpression {
2239 CompilerError.invariant(expr.property.type === 'Identifier', {
2240 reason: 'Expected JSX member expression property to be a string',
2241 loc: expr.loc ?? GeneratedSource,
2242 });
2243 const property = t.jsxIdentifier(expr.property.name);
2244 if (expr.object.type === 'Identifier') {
2245 return t.jsxMemberExpression(t.jsxIdentifier(expr.object.name), property);
2246 } else {
2247 CompilerError.invariant(expr.object.type === 'MemberExpression', {
2248 reason:
2249 'Expected JSX member expression to be an identifier or nested member expression',
2250 loc: expr.object.loc ?? GeneratedSource,
2251 });
2252 const object = convertMemberExpressionToJsx(expr.object);
2253 return t.jsxMemberExpression(object, property);
2254 }
2255 }
2256
2257 function codegenObjectPropertyKey(
2258 cx: Context,
2259 key: ObjectPropertyKey,
2260 ): t.Expression {
2261 switch (key.kind) {
2262 case 'string': {
2263 return t.stringLiteral(key.name);
2264 }
2265 case 'identifier': {
2266 return t.identifier(key.name);
2267 }
2268 case 'computed': {
2269 const expr = codegenPlace(cx, key.name);
2270 CompilerError.invariant(t.isExpression(expr), {
2271 reason: 'Expected object property key to be an expression',
2272 loc: key.name.loc,
2273 });
2274 return expr;
2275 }
2276 case 'number': {
2277 return t.numericLiteral(key.name);
2278 }
2279 }
2280 }
2281
2282 function codegenArrayPattern(
2283 cx: Context,
2284 pattern: ArrayPattern,
2285 ): t.ArrayPattern {
2286 const hasHoles = !pattern.items.every(e => e.kind !== 'Hole');
2287 if (hasHoles) {
2288 const result = createArrayPattern(pattern.loc, []);
2289 /*
2290 * Older versions of babel have a validation bug fixed by
2291 * https://github.com/babel/babel/pull/10917
2292 * https://github.com/babel/babel/commit/e7b80a2cb93cf28010207fc3cdd19b4568ca35b9#diff-19b555d2f3904c206af406540d9df200b1e16befedb83ff39ebfcbd876f7fa8aL52
2293 *
2294 * Link to buggy older version (observe that elements must be PatternLikes here)
2295 * https://github.com/babel/babel/blob/v7.7.4/packages/babel-types/src/definitions/es2015.js#L50-L53
2296 *
2297 * Link to newer versions with correct validation (observe elements can be PatternLike | null)
2298 * https://github.com/babel/babel/blob/v7.23.0/packages/babel-types/src/definitions/core.ts#L1306-L1311
2299 */
2300 for (const item of pattern.items) {
2301 if (item.kind === 'Hole') {
2302 result.elements.push(null);
2303 } else {
2304 result.elements.push(codegenLValue(cx, item));
2305 }
2306 }
2307 return result;
2308 } else {
2309 return createArrayPattern(
2310 pattern.loc,
2311 pattern.items.map(item => {
2312 if (item.kind === 'Hole') {
2313 return null;
2314 }
2315 return codegenLValue(cx, item);
2316 }),
2317 );
2318 }
2319 }
2320
2321 function codegenLValue(
2322 cx: Context,
2323 pattern: Pattern | Place | SpreadPattern,
2324 ): t.ArrayPattern | t.ObjectPattern | t.RestElement | t.Identifier {
2325 switch (pattern.kind) {
2326 case 'ArrayPattern': {
2327 return codegenArrayPattern(cx, pattern);
2328 }
2329 case 'ObjectPattern': {
2330 return createObjectPattern(
2331 pattern.loc,
2332 pattern.properties.map(property => {
2333 if (property.kind === 'ObjectProperty') {
2334 const key = codegenObjectPropertyKey(cx, property.key);
2335 const value = codegenLValue(cx, property.place);
2336 return t.objectProperty(
2337 key,
2338 value,
2339 property.key.kind === 'computed',
2340 key.type === 'Identifier' &&
2341 value.type === 'Identifier' &&
2342 value.name === key.name,
2343 );
2344 } else {
2345 return t.restElement(codegenLValue(cx, property.place));
2346 }
2347 }),
2348 );
2349 }
2350 case 'Spread': {
2351 return t.restElement(codegenLValue(cx, pattern.place));
2352 }
2353 case 'Identifier': {
2354 return convertIdentifier(pattern.identifier);
2355 }
2356 default: {
2357 assertExhaustive(
2358 pattern,
2359 `Unexpected pattern kind \`${(pattern as any).kind}\``,
2360 );
2361 }
2362 }
2363 }
2364
2365 function codegenValue(
2366 cx: Context,
2367 loc: SourceLocation,
2368 value: boolean | number | string | null | undefined,
2369 ): t.Expression {
2370 if (typeof value === 'number') {
2371 if (value < 0) {
2372 /**
2373 * Babel's code generator produces invalid JS for negative numbers when
2374 * run with { compact: true }.
2375 * See repro https://codesandbox.io/p/devbox/5d47fr
2376 */
2377 return t.unaryExpression('-', t.numericLiteral(-value), false);
2378 } else {
2379 return t.numericLiteral(value);
2380 }
2381 } else if (typeof value === 'boolean') {
2382 return t.booleanLiteral(value);
2383 } else if (typeof value === 'string') {
2384 return createStringLiteral(loc, value);
2385 } else if (value === null) {
2386 return t.nullLiteral();
2387 } else if (value === undefined) {
2388 return t.identifier('undefined');
2389 } else {
2390 assertExhaustive(value, 'Unexpected primitive value kind');
2391 }
2392 }
2393
2394 function codegenArgument(
2395 cx: Context,
2396 arg: Place | SpreadPattern,
2397 ): t.Expression | t.SpreadElement {
2398 if (arg.kind === 'Identifier') {
2399 return codegenPlaceToExpression(cx, arg);
2400 } else {
2401 return t.spreadElement(codegenPlaceToExpression(cx, arg.place));
2402 }
2403 }
2404
2405 function codegenPlaceToExpression(cx: Context, place: Place): t.Expression {
2406 const value = codegenPlace(cx, place);
2407 return convertValueToExpression(value);
2408 }
2409
2410 function codegenPlace(cx: Context, place: Place): t.Expression | t.JSXText {
2411 let tmp = cx.temp.get(place.identifier.declarationId);
2412 if (tmp != null) {
2413 return tmp;
2414 }
2415 CompilerError.invariant(place.identifier.name !== null || tmp !== undefined, {
2416 reason: `[Codegen] No value found for temporary`,
2417 description: `Value for '${printPlace(
2418 place,
2419 )}' was not set in the codegen context`,
2420 loc: place.loc,
2421 });
2422 const identifier = convertIdentifier(place.identifier);
2423 identifier.loc = place.loc as any;
2424 return identifier;
2425 }
2426
2427 function convertIdentifier(identifier: Identifier): t.Identifier {
2428 CompilerError.invariant(
2429 identifier.name !== null && identifier.name.kind === 'named',
2430 {
2431 reason: `Expected temporaries to be promoted to named identifiers in an earlier pass`,
2432 description: `identifier ${identifier.id} is unnamed`,
2433 loc: GeneratedSource,
2434 },
2435 );
2436 return createIdentifier(identifier.loc, identifier.name.value);
2437 }
2438
2439 function compareScopeDependency(
2440 a: ReactiveScopeDependency,
2441 b: ReactiveScopeDependency,
2442 ): number {
2443 CompilerError.invariant(
2444 a.identifier.name?.kind === 'named' && b.identifier.name?.kind === 'named',
2445 {
2446 reason: '[Codegen] Expected named identifier for dependency',
2447 loc: a.identifier.loc,
2448 },
2449 );
2450 const aName = [
2451 a.identifier.name.value,
2452 ...a.path.map(entry => `${entry.optional ? '?' : ''}${entry.property}`),
2453 ].join('.');
2454 const bName = [
2455 b.identifier.name.value,
2456 ...b.path.map(entry => `${entry.optional ? '?' : ''}${entry.property}`),
2457 ].join('.');
2458 if (aName < bName) return -1;
2459 else if (aName > bName) return 1;
2460 else return 0;
2461 }
2462
2463 function compareScopeDeclaration(
2464 a: ReactiveScopeDeclaration,
2465 b: ReactiveScopeDeclaration,
2466 ): number {
2467 CompilerError.invariant(
2468 a.identifier.name?.kind === 'named' && b.identifier.name?.kind === 'named',
2469 {
2470 reason: '[Codegen] Expected named identifier for declaration',
2471 loc: a.identifier.loc,
2472 },
2473 );
2474 const aName = a.identifier.name.value;
2475 const bName = b.identifier.name.value;
2476 if (aName < bName) return -1;
2477 else if (aName > bName) return 1;
2478 else return 0;
2479 }