main
ts 4,577 lines 145 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 {NodePath, Scope} from '@babel/traverse';
9 import * as t from '@babel/types';
10 import invariant from 'invariant';
11 import {
12 CompilerDiagnostic,
13 CompilerError,
14 CompilerErrorDetail,
15 CompilerSuggestionOperation,
16 ErrorCategory,
17 } from '../CompilerError';
18 import {assertExhaustive, hasNode} from '../Utils/utils';
19 import {Environment} from './Environment';
20 import {
21 ArrayExpression,
22 ArrayPattern,
23 BlockId,
24 BranchTerminal,
25 BuiltinTag,
26 Case,
27 Effect,
28 GeneratedSource,
29 GotoVariant,
30 HIRFunction,
31 IfTerminal,
32 InstructionKind,
33 InstructionValue,
34 JsxAttribute,
35 LoweredFunction,
36 ObjectPattern,
37 ObjectProperty,
38 ObjectPropertyKey,
39 Place,
40 PropertyLiteral,
41 ReturnTerminal,
42 SourceLocation,
43 SpreadPattern,
44 ThrowTerminal,
45 Type,
46 makeInstructionId,
47 makePropertyLiteral,
48 makeType,
49 promoteTemporary,
50 validateIdentifierName,
51 } from './HIR';
52 import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
53 import {BuiltInArrayId} from './ObjectShape';
54
55 /*
56 * *******************************************************************************************
57 * *******************************************************************************************
58 * ************************************* Lowering to HIR *************************************
59 * *******************************************************************************************
60 * *******************************************************************************************
61 */
62
63 /*
64 * Converts a function into a high-level intermediate form (HIR) which represents
65 * the code as a control-flow graph. All normal control-flow is modeled as accurately
66 * as possible to allow precise, expression-level memoization. The main exceptions are
67 * try/catch statements and exceptions: we currently bail out (skip compilation) for
68 * try/catch and do not attempt to model control flow of exceptions, which can occur
69 * ~anywhere in JavaScript. The compiler assumes that exceptions will be handled by
70 * the runtime, ie by invalidating memoization.
71 */
72 export function lower(
73 func: NodePath<t.Function>,
74 env: Environment,
75 // Bindings captured from the outer function, in case lower() is called recursively (for lambdas)
76 bindings: Bindings | null = null,
77 capturedRefs: Map<t.Identifier, SourceLocation> = new Map(),
78 ): HIRFunction {
79 const builder = new HIRBuilder(env, {
80 bindings,
81 context: capturedRefs,
82 });
83 const context: HIRFunction['context'] = [];
84
85 for (const [ref, loc] of capturedRefs ?? []) {
86 context.push({
87 kind: 'Identifier',
88 identifier: builder.resolveBinding(ref),
89 effect: Effect.Unknown,
90 reactive: false,
91 loc,
92 });
93 }
94
95 let id: string | null = null;
96 if (func.isFunctionDeclaration() || func.isFunctionExpression()) {
97 const idNode = (
98 func as NodePath<t.FunctionDeclaration | t.FunctionExpression>
99 ).get('id');
100 if (hasNode(idNode)) {
101 id = idNode.node.name;
102 }
103 }
104 const params: Array<Place | SpreadPattern> = [];
105 func.get('params').forEach(param => {
106 if (param.isIdentifier()) {
107 const binding = builder.resolveIdentifier(param);
108 if (binding.kind !== 'Identifier') {
109 builder.recordError(
110 CompilerDiagnostic.create({
111 category: ErrorCategory.Invariant,
112 reason: 'Could not find binding',
113 description: `[BuildHIR] Could not find binding for param \`${param.node.name}\``,
114 }).withDetails({
115 kind: 'error',
116 loc: param.node.loc ?? null,
117 message: 'Could not find binding',
118 }),
119 );
120 return;
121 }
122 const place: Place = {
123 kind: 'Identifier',
124 identifier: binding.identifier,
125 effect: Effect.Unknown,
126 reactive: false,
127 loc: param.node.loc ?? GeneratedSource,
128 };
129 params.push(place);
130 } else if (
131 param.isObjectPattern() ||
132 param.isArrayPattern() ||
133 param.isAssignmentPattern()
134 ) {
135 const place: Place = {
136 kind: 'Identifier',
137 identifier: builder.makeTemporary(param.node.loc ?? GeneratedSource),
138 effect: Effect.Unknown,
139 reactive: false,
140 loc: param.node.loc ?? GeneratedSource,
141 };
142 promoteTemporary(place.identifier);
143 params.push(place);
144 lowerAssignment(
145 builder,
146 param.node.loc ?? GeneratedSource,
147 InstructionKind.Let,
148 param,
149 place,
150 'Assignment',
151 );
152 } else if (param.isRestElement()) {
153 const place: Place = {
154 kind: 'Identifier',
155 identifier: builder.makeTemporary(param.node.loc ?? GeneratedSource),
156 effect: Effect.Unknown,
157 reactive: false,
158 loc: param.node.loc ?? GeneratedSource,
159 };
160 params.push({
161 kind: 'Spread',
162 place,
163 });
164 lowerAssignment(
165 builder,
166 param.node.loc ?? GeneratedSource,
167 InstructionKind.Let,
168 param.get('argument'),
169 place,
170 'Assignment',
171 );
172 } else {
173 builder.recordError(
174 CompilerDiagnostic.create({
175 category: ErrorCategory.Todo,
176 reason: `Handle ${param.node.type} parameters`,
177 description: `[BuildHIR] Add support for ${param.node.type} parameters`,
178 }).withDetails({
179 kind: 'error',
180 loc: param.node.loc ?? null,
181 message: 'Unsupported parameter type',
182 }),
183 );
184 }
185 });
186
187 let directives: Array<string> = [];
188 const body = func.get('body');
189 if (body.isExpression()) {
190 const fallthrough = builder.reserve('block');
191 const terminal: ReturnTerminal = {
192 kind: 'return',
193 returnVariant: 'Implicit',
194 loc: GeneratedSource,
195 value: lowerExpressionToTemporary(builder, body),
196 id: makeInstructionId(0),
197 effects: null,
198 };
199 builder.terminateWithContinuation(terminal, fallthrough);
200 } else if (body.isBlockStatement()) {
201 lowerStatement(builder, body);
202 directives = body.get('directives').map(d => d.node.value.value);
203 } else {
204 builder.recordError(
205 CompilerDiagnostic.create({
206 category: ErrorCategory.Syntax,
207 reason: `Unexpected function body kind`,
208 description: `Expected function body to be an expression or a block statement, got \`${body.type}\``,
209 }).withDetails({
210 kind: 'error',
211 loc: body.node.loc ?? null,
212 message: 'Expected a block statement or expression',
213 }),
214 );
215 }
216
217 let validatedId: HIRFunction['id'] = null;
218 if (id != null) {
219 const idResult = validateIdentifierName(id);
220 if (idResult.isErr()) {
221 for (const detail of idResult.unwrapErr().details) {
222 builder.recordError(detail);
223 }
224 } else {
225 validatedId = idResult.unwrap().value;
226 }
227 }
228
229 builder.terminate(
230 {
231 kind: 'return',
232 returnVariant: 'Void',
233 loc: GeneratedSource,
234 value: lowerValueToTemporary(builder, {
235 kind: 'Primitive',
236 value: undefined,
237 loc: GeneratedSource,
238 }),
239 id: makeInstructionId(0),
240 effects: null,
241 },
242 null,
243 );
244
245 const hirBody = builder.build();
246
247 return {
248 id: validatedId,
249 nameHint: null,
250 params,
251 fnType: bindings == null ? env.fnType : 'Other',
252 returnTypeAnnotation: null, // TODO: extract the actual return type node if present
253 returns: createTemporaryPlace(env, func.node.loc ?? GeneratedSource),
254 body: hirBody,
255 context,
256 generator: func.node.generator === true,
257 async: func.node.async === true,
258 loc: func.node.loc ?? GeneratedSource,
259 env,
260 aliasingEffects: null,
261 directives,
262 };
263 }
264
265 // Helper to lower a statement
266 function lowerStatement(
267 builder: HIRBuilder,
268 stmtPath: NodePath<t.Statement>,
269 label: string | null = null,
270 ): void {
271 const stmtNode = stmtPath.node;
272 switch (stmtNode.type) {
273 case 'ThrowStatement': {
274 const stmt = stmtPath as NodePath<t.ThrowStatement>;
275 const value = lowerExpressionToTemporary(builder, stmt.get('argument'));
276 const handler = builder.resolveThrowHandler();
277 if (handler != null) {
278 /*
279 * NOTE: we could support this, but a `throw` inside try/catch is using exceptions
280 * for control-flow and is generally considered an anti-pattern. we can likely
281 * just not support this pattern, unless it really becomes necessary for some reason.
282 */
283 builder.recordError(
284 new CompilerErrorDetail({
285 reason:
286 '(BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch',
287 category: ErrorCategory.Todo,
288 loc: stmt.node.loc ?? null,
289 suggestions: null,
290 }),
291 );
292 }
293 const terminal: ThrowTerminal = {
294 kind: 'throw',
295 value,
296 id: makeInstructionId(0),
297 loc: stmt.node.loc ?? GeneratedSource,
298 };
299 builder.terminate(terminal, 'block');
300 return;
301 }
302 case 'ReturnStatement': {
303 const stmt = stmtPath as NodePath<t.ReturnStatement>;
304 const argument = stmt.get('argument');
305 let value;
306 if (argument.node === null) {
307 value = lowerValueToTemporary(builder, {
308 kind: 'Primitive',
309 value: undefined,
310 loc: GeneratedSource,
311 });
312 } else {
313 value = lowerExpressionToTemporary(
314 builder,
315 argument as NodePath<t.Expression>,
316 );
317 }
318 const terminal: ReturnTerminal = {
319 kind: 'return',
320 returnVariant: 'Explicit',
321 loc: stmt.node.loc ?? GeneratedSource,
322 value,
323 id: makeInstructionId(0),
324 effects: null,
325 };
326 builder.terminate(terminal, 'block');
327 return;
328 }
329 case 'IfStatement': {
330 const stmt = stmtPath as NodePath<t.IfStatement>;
331 // Block for code following the if
332 const continuationBlock = builder.reserve('block');
333 // Block for the consequent (if the test is truthy)
334 const consequentBlock = builder.enter('block', _blockId => {
335 const consequent = stmt.get('consequent');
336 lowerStatement(builder, consequent);
337 return {
338 kind: 'goto',
339 block: continuationBlock.id,
340 variant: GotoVariant.Break,
341 id: makeInstructionId(0),
342 loc: consequent.node.loc ?? GeneratedSource,
343 };
344 });
345 // Block for the alternate (if the test is not truthy)
346 let alternateBlock: BlockId;
347 const alternate = stmt.get('alternate');
348 if (hasNode(alternate)) {
349 alternateBlock = builder.enter('block', _blockId => {
350 lowerStatement(builder, alternate);
351 return {
352 kind: 'goto',
353 block: continuationBlock.id,
354 variant: GotoVariant.Break,
355 id: makeInstructionId(0),
356 loc: alternate.node?.loc ?? GeneratedSource,
357 };
358 });
359 } else {
360 // If there is no else clause, use the continuation directly
361 alternateBlock = continuationBlock.id;
362 }
363 const test = lowerExpressionToTemporary(builder, stmt.get('test'));
364 const terminal: IfTerminal = {
365 kind: 'if',
366 test,
367 consequent: consequentBlock,
368 alternate: alternateBlock,
369 fallthrough: continuationBlock.id,
370 id: makeInstructionId(0),
371 loc: stmt.node.loc ?? GeneratedSource,
372 };
373 builder.terminateWithContinuation(terminal, continuationBlock);
374 return;
375 }
376 case 'BlockStatement': {
377 const stmt = stmtPath as NodePath<t.BlockStatement>;
378 const statements = stmt.get('body');
379 /**
380 * Hoistable identifier bindings defined for this precise block
381 * scope (excluding bindings from parent or child block scopes).
382 */
383 const hoistableIdentifiers: Set<t.Identifier> = new Set();
384
385 for (const [, binding] of Object.entries(stmt.scope.bindings)) {
386 // refs to params are always valid / never need to be hoisted
387 if (binding.kind !== 'param') {
388 hoistableIdentifiers.add(binding.identifier);
389 }
390 }
391
392 for (const s of statements) {
393 const willHoist = new Set<NodePath<t.Identifier>>();
394 /*
395 * If we see a hoistable identifier before its declaration, it should be hoisted just
396 * before the statement that references it.
397 */
398 let fnDepth = s.isFunctionDeclaration() ? 1 : 0;
399 const withFunctionContext = {
400 enter: (): void => {
401 fnDepth++;
402 },
403 exit: (): void => {
404 fnDepth--;
405 },
406 };
407 s.traverse({
408 FunctionExpression: withFunctionContext,
409 FunctionDeclaration: withFunctionContext,
410 ArrowFunctionExpression: withFunctionContext,
411 ObjectMethod: withFunctionContext,
412 Identifier(id: NodePath<t.Identifier>) {
413 const id2 = id;
414 if (
415 !id2.isReferencedIdentifier() &&
416 // isReferencedIdentifier is broken and returns false for reassignments
417 id.parent.type !== 'AssignmentExpression'
418 ) {
419 return;
420 }
421 const binding = id.scope.getBinding(id.node.name);
422 /**
423 * We can only hoist an identifier decl if
424 * 1. the reference occurs within an inner function
425 * or
426 * 2. the declaration itself is hoistable
427 */
428 if (
429 binding != null &&
430 hoistableIdentifiers.has(binding.identifier) &&
431 (fnDepth > 0 || binding.kind === 'hoisted')
432 ) {
433 willHoist.add(id);
434 }
435 },
436 });
437 /*
438 * After visiting the declaration, hoisting is no longer required
439 */
440 s.traverse({
441 Identifier(path: NodePath<t.Identifier>) {
442 if (hoistableIdentifiers.has(path.node)) {
443 hoistableIdentifiers.delete(path.node);
444 }
445 },
446 });
447
448 // Hoist declarations that need it to the earliest point where they are needed
449 for (const id of willHoist) {
450 const binding = stmt.scope.getBinding(id.node.name);
451 CompilerError.invariant(binding != null, {
452 reason: 'Expected to find binding for hoisted identifier',
453 description: `Could not find a binding for ${id.node.name}`,
454 loc: id.node.loc ?? GeneratedSource,
455 });
456 if (builder.environment.isHoistedIdentifier(binding.identifier)) {
457 // Already hoisted
458 continue;
459 }
460
461 let kind:
462 | InstructionKind.Let
463 | InstructionKind.HoistedConst
464 | InstructionKind.HoistedLet
465 | InstructionKind.HoistedFunction;
466 if (binding.kind === 'const' || binding.kind === 'var') {
467 kind = InstructionKind.HoistedConst;
468 } else if (binding.kind === 'let') {
469 kind = InstructionKind.HoistedLet;
470 } else if (binding.path.isFunctionDeclaration()) {
471 kind = InstructionKind.HoistedFunction;
472 } else if (!binding.path.isVariableDeclarator()) {
473 builder.recordError(
474 new CompilerErrorDetail({
475 category: ErrorCategory.Todo,
476 reason: 'Unsupported declaration type for hoisting',
477 description: `variable "${binding.identifier.name}" declared with ${binding.path.type}`,
478 suggestions: null,
479 loc: id.parentPath.node.loc ?? GeneratedSource,
480 }),
481 );
482 continue;
483 } else {
484 builder.recordError(
485 new CompilerErrorDetail({
486 category: ErrorCategory.Todo,
487 reason: 'Handle non-const declarations for hoisting',
488 description: `variable "${binding.identifier.name}" declared with ${binding.kind}`,
489 suggestions: null,
490 loc: id.parentPath.node.loc ?? GeneratedSource,
491 }),
492 );
493 continue;
494 }
495
496 const identifier = builder.resolveIdentifier(id);
497 CompilerError.invariant(identifier.kind === 'Identifier', {
498 reason:
499 'Expected hoisted binding to be a local identifier, not a global',
500 loc: id.node.loc ?? GeneratedSource,
501 });
502 const place: Place = {
503 effect: Effect.Unknown,
504 identifier: identifier.identifier,
505 kind: 'Identifier',
506 reactive: false,
507 loc: id.node.loc ?? GeneratedSource,
508 };
509 lowerValueToTemporary(builder, {
510 kind: 'DeclareContext',
511 lvalue: {
512 kind,
513 place,
514 },
515 loc: id.node.loc ?? GeneratedSource,
516 });
517 builder.environment.addHoistedIdentifier(binding.identifier);
518 }
519 lowerStatement(builder, s);
520 }
521
522 return;
523 }
524 case 'BreakStatement': {
525 const stmt = stmtPath as NodePath<t.BreakStatement>;
526 const block = builder.lookupBreak(stmt.node.label?.name ?? null);
527 builder.terminate(
528 {
529 kind: 'goto',
530 block,
531 variant: GotoVariant.Break,
532 id: makeInstructionId(0),
533 loc: stmt.node.loc ?? GeneratedSource,
534 },
535 'block',
536 );
537 return;
538 }
539 case 'ContinueStatement': {
540 const stmt = stmtPath as NodePath<t.ContinueStatement>;
541 const block = builder.lookupContinue(stmt.node.label?.name ?? null);
542 builder.terminate(
543 {
544 kind: 'goto',
545 block,
546 variant: GotoVariant.Continue,
547 id: makeInstructionId(0),
548 loc: stmt.node.loc ?? GeneratedSource,
549 },
550 'block',
551 );
552 return;
553 }
554 case 'ForStatement': {
555 const stmt = stmtPath as NodePath<t.ForStatement>;
556
557 const testBlock = builder.reserve('loop');
558 // Block for code following the loop
559 const continuationBlock = builder.reserve('block');
560
561 const initBlock = builder.enter('loop', _blockId => {
562 const init = stmt.get('init');
563 if (init.node == null) {
564 /*
565 * No init expression (e.g., `for (; ...)`), add a placeholder to avoid
566 * invariant about empty blocks
567 */
568 lowerValueToTemporary(builder, {
569 kind: 'Primitive',
570 value: undefined,
571 loc: stmt.node.loc ?? GeneratedSource,
572 });
573 return {
574 kind: 'goto',
575 block: testBlock.id,
576 variant: GotoVariant.Break,
577 id: makeInstructionId(0),
578 loc: stmt.node.loc ?? GeneratedSource,
579 };
580 }
581 if (!init.isVariableDeclaration()) {
582 builder.recordError(
583 new CompilerErrorDetail({
584 reason:
585 '(BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement',
586 category: ErrorCategory.Todo,
587 loc: stmt.node.loc ?? null,
588 suggestions: null,
589 }),
590 );
591 // Lower the init expression as best-effort and continue
592 if (init.isExpression()) {
593 lowerExpressionToTemporary(builder, init as NodePath<t.Expression>);
594 }
595 return {
596 kind: 'goto',
597 block: testBlock.id,
598 variant: GotoVariant.Break,
599 id: makeInstructionId(0),
600 loc: init.node?.loc ?? GeneratedSource,
601 };
602 }
603 lowerStatement(builder, init);
604 return {
605 kind: 'goto',
606 block: testBlock.id,
607 variant: GotoVariant.Break,
608 id: makeInstructionId(0),
609 loc: init.node.loc ?? GeneratedSource,
610 };
611 });
612
613 let updateBlock: BlockId | null = null;
614 const update = stmt.get('update');
615 if (hasNode(update)) {
616 updateBlock = builder.enter('loop', _blockId => {
617 lowerExpressionToTemporary(builder, update);
618 return {
619 kind: 'goto',
620 block: testBlock.id,
621 variant: GotoVariant.Break,
622 id: makeInstructionId(0),
623 loc: update.node?.loc ?? GeneratedSource,
624 };
625 });
626 }
627
628 const bodyBlock = builder.enter('block', _blockId => {
629 return builder.loop(
630 label,
631 updateBlock ?? testBlock.id,
632 continuationBlock.id,
633 () => {
634 const body = stmt.get('body');
635 lowerStatement(builder, body);
636 return {
637 kind: 'goto',
638 block: updateBlock ?? testBlock.id,
639 variant: GotoVariant.Continue,
640 id: makeInstructionId(0),
641 loc: body.node.loc ?? GeneratedSource,
642 };
643 },
644 );
645 });
646
647 builder.terminateWithContinuation(
648 {
649 kind: 'for',
650 loc: stmtNode.loc ?? GeneratedSource,
651 init: initBlock,
652 test: testBlock.id,
653 update: updateBlock,
654 loop: bodyBlock,
655 fallthrough: continuationBlock.id,
656 id: makeInstructionId(0),
657 },
658 testBlock,
659 );
660
661 const test = stmt.get('test');
662 if (test.node == null) {
663 builder.recordError(
664 new CompilerErrorDetail({
665 reason: `(BuildHIR::lowerStatement) Handle empty test in ForStatement`,
666 category: ErrorCategory.Todo,
667 loc: stmt.node.loc ?? null,
668 suggestions: null,
669 }),
670 );
671 // Treat `for(;;)` as `while(true)` to keep the builder state consistent
672 builder.terminateWithContinuation(
673 {
674 kind: 'branch',
675 test: lowerValueToTemporary(builder, {
676 kind: 'Primitive',
677 value: true,
678 loc: stmt.node.loc ?? GeneratedSource,
679 }),
680 consequent: bodyBlock,
681 alternate: continuationBlock.id,
682 fallthrough: continuationBlock.id,
683 id: makeInstructionId(0),
684 loc: stmt.node.loc ?? GeneratedSource,
685 },
686 continuationBlock,
687 );
688 } else {
689 builder.terminateWithContinuation(
690 {
691 kind: 'branch',
692 test: lowerExpressionToTemporary(
693 builder,
694 test as NodePath<t.Expression>,
695 ),
696 consequent: bodyBlock,
697 alternate: continuationBlock.id,
698 fallthrough: continuationBlock.id,
699 id: makeInstructionId(0),
700 loc: stmt.node.loc ?? GeneratedSource,
701 },
702 continuationBlock,
703 );
704 }
705 return;
706 }
707 case 'WhileStatement': {
708 const stmt = stmtPath as NodePath<t.WhileStatement>;
709 // Block used to evaluate whether to (re)enter or exit the loop
710 const conditionalBlock = builder.reserve('loop');
711 // Block for code following the loop
712 const continuationBlock = builder.reserve('block');
713 // Loop body
714 const loopBlock = builder.enter('block', _blockId => {
715 return builder.loop(
716 label,
717 conditionalBlock.id,
718 continuationBlock.id,
719 () => {
720 const body = stmt.get('body');
721 lowerStatement(builder, body);
722 return {
723 kind: 'goto',
724 block: conditionalBlock.id,
725 variant: GotoVariant.Continue,
726 id: makeInstructionId(0),
727 loc: body.node.loc ?? GeneratedSource,
728 };
729 },
730 );
731 });
732 /*
733 * The code leading up to the loop must jump to the conditional block,
734 * to evaluate whether to enter the loop or bypass to the continuation.
735 */
736 const loc = stmt.node.loc ?? GeneratedSource;
737 builder.terminateWithContinuation(
738 {
739 kind: 'while',
740 loc,
741 test: conditionalBlock.id,
742 loop: loopBlock,
743 fallthrough: continuationBlock.id,
744 id: makeInstructionId(0),
745 },
746 conditionalBlock,
747 );
748 const test = lowerExpressionToTemporary(builder, stmt.get('test'));
749 const terminal: BranchTerminal = {
750 kind: 'branch',
751 test,
752 consequent: loopBlock,
753 alternate: continuationBlock.id,
754 fallthrough: conditionalBlock.id,
755 id: makeInstructionId(0),
756 loc: stmt.node.loc ?? GeneratedSource,
757 };
758 // Complete the conditional and continue with code after the loop
759 builder.terminateWithContinuation(terminal, continuationBlock);
760 return;
761 }
762 case 'LabeledStatement': {
763 const stmt = stmtPath as NodePath<t.LabeledStatement>;
764 const label = stmt.node.label.name;
765 const body = stmt.get('body');
766 switch (body.node.type) {
767 case 'ForInStatement':
768 case 'ForOfStatement':
769 case 'ForStatement':
770 case 'WhileStatement':
771 case 'DoWhileStatement': {
772 /*
773 * labeled loops are special because of continue, so push the label
774 * down
775 */
776 lowerStatement(builder, stmt.get('body'), label);
777 break;
778 }
779 default: {
780 /*
781 * All other statements create a continuation block to allow `break`,
782 * explicitly *don't* pass the label down
783 */
784 const continuationBlock = builder.reserve('block');
785 const block = builder.enter('block', () => {
786 const body = stmt.get('body');
787 builder.label(label, continuationBlock.id, () => {
788 lowerStatement(builder, body);
789 });
790 return {
791 kind: 'goto',
792 block: continuationBlock.id,
793 variant: GotoVariant.Break,
794 id: makeInstructionId(0),
795 loc: body.node.loc ?? GeneratedSource,
796 };
797 });
798 builder.terminateWithContinuation(
799 {
800 kind: 'label',
801 block,
802 fallthrough: continuationBlock.id,
803 id: makeInstructionId(0),
804 loc: stmt.node.loc ?? GeneratedSource,
805 },
806 continuationBlock,
807 );
808 }
809 }
810 return;
811 }
812 case 'SwitchStatement': {
813 const stmt = stmtPath as NodePath<t.SwitchStatement>;
814 // Block following the switch
815 const continuationBlock = builder.reserve('block');
816 /*
817 * The goto target for any cases that fallthrough, which initially starts
818 * as the continuation block and is then updated as we iterate through cases
819 * in reverse order.
820 */
821 let fallthrough = continuationBlock.id;
822 /*
823 * Iterate through cases in reverse order, so that previous blocks can fallthrough
824 * to successors
825 */
826 const cases: Array<Case> = [];
827 let hasDefault = false;
828 for (let ii = stmt.get('cases').length - 1; ii >= 0; ii--) {
829 const case_: NodePath<t.SwitchCase> = stmt.get('cases')[ii];
830 const testExpr = case_.get('test');
831 if (testExpr.node == null) {
832 if (hasDefault) {
833 builder.recordError(
834 new CompilerErrorDetail({
835 reason: `Expected at most one \`default\` branch in a switch statement, this code should have failed to parse`,
836 category: ErrorCategory.Syntax,
837 loc: case_.node.loc ?? null,
838 suggestions: null,
839 }),
840 );
841 break;
842 }
843 hasDefault = true;
844 }
845 const block = builder.enter('block', _blockId => {
846 return builder.switch(label, continuationBlock.id, () => {
847 case_
848 .get('consequent')
849 .forEach(consequent => lowerStatement(builder, consequent));
850 /*
851 * always generate a fallthrough to the next block, this may be dead code
852 * if there was an explicit break, but if so it will be pruned later.
853 */
854 return {
855 kind: 'goto',
856 block: fallthrough,
857 variant: GotoVariant.Break,
858 id: makeInstructionId(0),
859 loc: case_.node.loc ?? GeneratedSource,
860 };
861 });
862 });
863 let test: Place | null = null;
864 if (hasNode(testExpr)) {
865 test = lowerReorderableExpression(builder, testExpr);
866 }
867 cases.push({
868 test,
869 block,
870 });
871 fallthrough = block;
872 }
873 /*
874 * it doesn't matter for our analysis purposes, but reverse the order of the cases
875 * back to the original to make it match the original code/intent.
876 */
877 cases.reverse();
878 /*
879 * If there wasn't an explicit default case, generate one to model the fact that execution
880 * could bypass any of the other cases and jump directly to the continuation.
881 */
882 if (!hasDefault) {
883 cases.push({test: null, block: continuationBlock.id});
884 }
885
886 const test = lowerExpressionToTemporary(
887 builder,
888 stmt.get('discriminant'),
889 );
890 builder.terminateWithContinuation(
891 {
892 kind: 'switch',
893 test,
894 cases,
895 fallthrough: continuationBlock.id,
896 id: makeInstructionId(0),
897 loc: stmt.node.loc ?? GeneratedSource,
898 },
899 continuationBlock,
900 );
901 return;
902 }
903 case 'VariableDeclaration': {
904 const stmt = stmtPath as NodePath<t.VariableDeclaration>;
905 const nodeKind: t.VariableDeclaration['kind'] = stmt.node.kind;
906 if (
907 nodeKind === 'var' ||
908 nodeKind === 'using' ||
909 nodeKind === 'await using'
910 ) {
911 builder.recordError(
912 new CompilerErrorDetail({
913 reason: `(BuildHIR::lowerStatement) Handle ${nodeKind} kinds in VariableDeclaration`,
914 category: ErrorCategory.Todo,
915 loc: stmt.node.loc ?? null,
916 suggestions: null,
917 }),
918 );
919 /*
920 * Treat `var` as `let` and `using`/`await using` as `const` so
921 * references to the variable don't break while the error unwinds
922 */
923 }
924 const kind =
925 nodeKind === 'let' || nodeKind === 'var'
926 ? InstructionKind.Let
927 : InstructionKind.Const;
928 for (const declaration of stmt.get('declarations')) {
929 const id = declaration.get('id');
930 const init = declaration.get('init');
931 if (hasNode(init)) {
932 const value = lowerExpressionToTemporary(builder, init);
933 lowerAssignment(
934 builder,
935 stmt.node.loc ?? GeneratedSource,
936 kind,
937 id,
938 value,
939 id.isObjectPattern() || id.isArrayPattern()
940 ? 'Destructure'
941 : 'Assignment',
942 );
943 } else if (id.isIdentifier()) {
944 const binding = builder.resolveIdentifier(id);
945 if (binding.kind !== 'Identifier') {
946 builder.recordError(
947 new CompilerErrorDetail({
948 reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
949 category: ErrorCategory.Invariant,
950 loc: id.node.loc ?? null,
951 suggestions: null,
952 }),
953 );
954 } else {
955 const place: Place = {
956 effect: Effect.Unknown,
957 identifier: binding.identifier,
958 kind: 'Identifier',
959 reactive: false,
960 loc: id.node.loc ?? GeneratedSource,
961 };
962 if (builder.isContextIdentifier(id)) {
963 if (kind === InstructionKind.Const) {
964 const declRangeStart = declaration.parentPath.node.start!;
965 builder.recordError(
966 new CompilerErrorDetail({
967 reason: `Expect \`const\` declaration not to be reassigned`,
968 category: ErrorCategory.Syntax,
969 loc: id.node.loc ?? null,
970 suggestions: [
971 {
972 description: 'Change to a `let` declaration',
973 op: CompilerSuggestionOperation.Replace,
974 range: [declRangeStart, declRangeStart + 5], // "const".length
975 text: 'let',
976 },
977 ],
978 }),
979 );
980 }
981 lowerValueToTemporary(builder, {
982 kind: 'DeclareContext',
983 lvalue: {
984 kind: InstructionKind.Let,
985 place,
986 },
987 loc: id.node.loc ?? GeneratedSource,
988 });
989 } else {
990 const typeAnnotation = id.get('typeAnnotation');
991 let type: t.FlowType | t.TSType | null;
992 if (typeAnnotation.isTSTypeAnnotation()) {
993 const typePath = typeAnnotation.get('typeAnnotation');
994 type = typePath.node;
995 } else if (typeAnnotation.isTypeAnnotation()) {
996 const typePath = typeAnnotation.get('typeAnnotation');
997 type = typePath.node;
998 } else {
999 type = null;
1000 }
1001 lowerValueToTemporary(builder, {
1002 kind: 'DeclareLocal',
1003 lvalue: {
1004 kind,
1005 place,
1006 },
1007 type,
1008 loc: id.node.loc ?? GeneratedSource,
1009 });
1010 }
1011 }
1012 } else {
1013 builder.recordError(
1014 new CompilerErrorDetail({
1015 reason: `Expected variable declaration to be an identifier if no initializer was provided`,
1016 description: `Got a \`${id.type}\``,
1017 category: ErrorCategory.Syntax,
1018 loc: stmt.node.loc ?? null,
1019 suggestions: null,
1020 }),
1021 );
1022 }
1023 }
1024 return;
1025 }
1026 case 'ExpressionStatement': {
1027 const stmt = stmtPath as NodePath<t.ExpressionStatement>;
1028 const expression = stmt.get('expression');
1029 lowerExpressionToTemporary(builder, expression);
1030 return;
1031 }
1032 case 'DoWhileStatement': {
1033 const stmt = stmtPath as NodePath<t.DoWhileStatement>;
1034 // Block used to evaluate whether to (re)enter or exit the loop
1035 const conditionalBlock = builder.reserve('loop');
1036 // Block for code following the loop
1037 const continuationBlock = builder.reserve('block');
1038 // Loop body, executed at least once uncondtionally prior to exit
1039 const loopBlock = builder.enter('block', _loopBlockId => {
1040 return builder.loop(
1041 label,
1042 conditionalBlock.id,
1043 continuationBlock.id,
1044 () => {
1045 const body = stmt.get('body');
1046 lowerStatement(builder, body);
1047 return {
1048 kind: 'goto',
1049 block: conditionalBlock.id,
1050 variant: GotoVariant.Continue,
1051 id: makeInstructionId(0),
1052 loc: body.node.loc ?? GeneratedSource,
1053 };
1054 },
1055 );
1056 });
1057 /*
1058 * Jump to the conditional block to evaluate whether to (re)enter the loop or exit to the
1059 * continuation block.
1060 */
1061 const loc = stmt.node.loc ?? GeneratedSource;
1062 builder.terminateWithContinuation(
1063 {
1064 kind: 'do-while',
1065 loc,
1066 test: conditionalBlock.id,
1067 loop: loopBlock,
1068 fallthrough: continuationBlock.id,
1069 id: makeInstructionId(0),
1070 },
1071 conditionalBlock,
1072 );
1073 /*
1074 * The conditional block is empty and exists solely as conditional for
1075 * (re)entering or exiting the loop
1076 */
1077 const test = lowerExpressionToTemporary(builder, stmt.get('test'));
1078 const terminal: BranchTerminal = {
1079 kind: 'branch',
1080 test,
1081 consequent: loopBlock,
1082 alternate: continuationBlock.id,
1083 fallthrough: conditionalBlock.id,
1084 id: makeInstructionId(0),
1085 loc,
1086 };
1087 // Complete the conditional and continue with code after the loop
1088 builder.terminateWithContinuation(terminal, continuationBlock);
1089 return;
1090 }
1091 case 'FunctionDeclaration': {
1092 const stmt = stmtPath as NodePath<t.FunctionDeclaration>;
1093 stmt.skip();
1094 CompilerError.invariant(stmt.get('id').type === 'Identifier', {
1095 reason: 'function declarations must have a name',
1096 loc: stmt.node.loc ?? GeneratedSource,
1097 });
1098 const id = stmt.get('id') as NodePath<t.Identifier>;
1099
1100 const fn = lowerValueToTemporary(
1101 builder,
1102 lowerFunctionToValue(builder, stmt),
1103 );
1104 lowerAssignment(
1105 builder,
1106 stmt.node.loc ?? GeneratedSource,
1107 InstructionKind.Function,
1108 id,
1109 fn,
1110 'Assignment',
1111 );
1112
1113 return;
1114 }
1115 case 'ForOfStatement': {
1116 const stmt = stmtPath as NodePath<t.ForOfStatement>;
1117 const continuationBlock = builder.reserve('block');
1118 const initBlock = builder.reserve('loop');
1119 const testBlock = builder.reserve('loop');
1120
1121 if (stmt.node.await) {
1122 builder.recordError(
1123 new CompilerErrorDetail({
1124 reason: `(BuildHIR::lowerStatement) Handle for-await loops`,
1125 category: ErrorCategory.Todo,
1126 loc: stmt.node.loc ?? null,
1127 suggestions: null,
1128 }),
1129 );
1130 return;
1131 }
1132
1133 const loopBlock = builder.enter('block', _blockId => {
1134 return builder.loop(label, initBlock.id, continuationBlock.id, () => {
1135 const body = stmt.get('body');
1136 lowerStatement(builder, body);
1137 return {
1138 kind: 'goto',
1139 block: initBlock.id,
1140 variant: GotoVariant.Continue,
1141 id: makeInstructionId(0),
1142 loc: body.node.loc ?? GeneratedSource,
1143 };
1144 });
1145 });
1146
1147 const loc = stmt.node.loc ?? GeneratedSource;
1148 const value = lowerExpressionToTemporary(builder, stmt.get('right'));
1149 builder.terminateWithContinuation(
1150 {
1151 kind: 'for-of',
1152 loc,
1153 init: initBlock.id,
1154 test: testBlock.id,
1155 loop: loopBlock,
1156 fallthrough: continuationBlock.id,
1157 id: makeInstructionId(0),
1158 },
1159 initBlock,
1160 );
1161
1162 /*
1163 * The init of a ForOf statement is compound over a left (VariableDeclaration | LVal) and
1164 * right (Expression), so we synthesize a new InstrValue and assignment (potentially multiple
1165 * instructions when we handle other syntax like Patterns)
1166 */
1167 const iterator = lowerValueToTemporary(builder, {
1168 kind: 'GetIterator',
1169 loc: value.loc,
1170 collection: {...value},
1171 });
1172 builder.terminateWithContinuation(
1173 {
1174 id: makeInstructionId(0),
1175 kind: 'goto',
1176 block: testBlock.id,
1177 variant: GotoVariant.Break,
1178 loc: stmt.node.loc ?? GeneratedSource,
1179 },
1180 testBlock,
1181 );
1182
1183 const left = stmt.get('left');
1184 const leftLoc = left.node.loc ?? GeneratedSource;
1185 let test: Place;
1186 const advanceIterator = lowerValueToTemporary(builder, {
1187 kind: 'IteratorNext',
1188 loc: leftLoc,
1189 iterator: {...iterator},
1190 collection: {...value},
1191 });
1192 if (left.isVariableDeclaration()) {
1193 const declarations = left.get('declarations');
1194 CompilerError.invariant(declarations.length === 1, {
1195 reason: `Expected only one declaration in the init of a ForOfStatement, got ${declarations.length}`,
1196 loc: left.node.loc ?? GeneratedSource,
1197 });
1198 const id = declarations[0].get('id');
1199 const assign = lowerAssignment(
1200 builder,
1201 leftLoc,
1202 InstructionKind.Let,
1203 id,
1204 advanceIterator,
1205 'Assignment',
1206 );
1207 test = lowerValueToTemporary(builder, assign);
1208 } else {
1209 CompilerError.invariant(left.isLVal(), {
1210 reason: 'Expected ForOf init to be a variable declaration or lval',
1211 loc: leftLoc,
1212 });
1213 const assign = lowerAssignment(
1214 builder,
1215 leftLoc,
1216 InstructionKind.Reassign,
1217 left,
1218 advanceIterator,
1219 'Assignment',
1220 );
1221 test = lowerValueToTemporary(builder, assign);
1222 }
1223 builder.terminateWithContinuation(
1224 {
1225 id: makeInstructionId(0),
1226 kind: 'branch',
1227 test,
1228 consequent: loopBlock,
1229 alternate: continuationBlock.id,
1230 loc: stmt.node.loc ?? GeneratedSource,
1231 fallthrough: continuationBlock.id,
1232 },
1233 continuationBlock,
1234 );
1235 return;
1236 }
1237 case 'ForInStatement': {
1238 const stmt = stmtPath as NodePath<t.ForInStatement>;
1239 const continuationBlock = builder.reserve('block');
1240 const initBlock = builder.reserve('loop');
1241
1242 const loopBlock = builder.enter('block', _blockId => {
1243 return builder.loop(label, initBlock.id, continuationBlock.id, () => {
1244 const body = stmt.get('body');
1245 lowerStatement(builder, body);
1246 return {
1247 kind: 'goto',
1248 block: initBlock.id,
1249 variant: GotoVariant.Continue,
1250 id: makeInstructionId(0),
1251 loc: body.node.loc ?? GeneratedSource,
1252 };
1253 });
1254 });
1255
1256 const loc = stmt.node.loc ?? GeneratedSource;
1257 const value = lowerExpressionToTemporary(builder, stmt.get('right'));
1258 builder.terminateWithContinuation(
1259 {
1260 kind: 'for-in',
1261 loc,
1262 init: initBlock.id,
1263 loop: loopBlock,
1264 fallthrough: continuationBlock.id,
1265 id: makeInstructionId(0),
1266 },
1267 initBlock,
1268 );
1269
1270 /*
1271 * The init of a ForIn statement is compound over a left (VariableDeclaration | LVal) and
1272 * right (Expression), so we synthesize a new InstrValue and assignment (potentially multiple
1273 * instructions when we handle other syntax like Patterns)
1274 */
1275 const left = stmt.get('left');
1276 const leftLoc = left.node.loc ?? GeneratedSource;
1277 let test: Place;
1278 const nextPropertyTemp = lowerValueToTemporary(builder, {
1279 kind: 'NextPropertyOf',
1280 loc: leftLoc,
1281 value,
1282 });
1283 if (left.isVariableDeclaration()) {
1284 const declarations = left.get('declarations');
1285 CompilerError.invariant(declarations.length === 1, {
1286 reason: `Expected only one declaration in the init of a ForInStatement, got ${declarations.length}`,
1287 loc: left.node.loc ?? GeneratedSource,
1288 });
1289 const id = declarations[0].get('id');
1290 const assign = lowerAssignment(
1291 builder,
1292 leftLoc,
1293 InstructionKind.Let,
1294 id,
1295 nextPropertyTemp,
1296 'Assignment',
1297 );
1298 test = lowerValueToTemporary(builder, assign);
1299 } else {
1300 CompilerError.invariant(left.isLVal(), {
1301 reason: 'Expected ForIn init to be a variable declaration or lval',
1302 loc: leftLoc,
1303 });
1304 const assign = lowerAssignment(
1305 builder,
1306 leftLoc,
1307 InstructionKind.Reassign,
1308 left,
1309 nextPropertyTemp,
1310 'Assignment',
1311 );
1312 test = lowerValueToTemporary(builder, assign);
1313 }
1314 builder.terminateWithContinuation(
1315 {
1316 id: makeInstructionId(0),
1317 kind: 'branch',
1318 test,
1319 consequent: loopBlock,
1320 alternate: continuationBlock.id,
1321 fallthrough: continuationBlock.id,
1322 loc: stmt.node.loc ?? GeneratedSource,
1323 },
1324 continuationBlock,
1325 );
1326 return;
1327 }
1328 case 'DebuggerStatement': {
1329 const stmt = stmtPath as NodePath<t.DebuggerStatement>;
1330 const loc = stmt.node.loc ?? GeneratedSource;
1331 builder.push({
1332 id: makeInstructionId(0),
1333 lvalue: buildTemporaryPlace(builder, loc),
1334 value: {
1335 kind: 'Debugger',
1336 loc,
1337 },
1338 effects: null,
1339 loc,
1340 });
1341 return;
1342 }
1343 case 'EmptyStatement': {
1344 return;
1345 }
1346 case 'TryStatement': {
1347 const stmt = stmtPath as NodePath<t.TryStatement>;
1348 const continuationBlock = builder.reserve('block');
1349
1350 const handlerPath = stmt.get('handler');
1351 if (!hasNode(handlerPath)) {
1352 builder.recordError(
1353 new CompilerErrorDetail({
1354 reason: `(BuildHIR::lowerStatement) Handle TryStatement without a catch clause`,
1355 category: ErrorCategory.Todo,
1356 loc: stmt.node.loc ?? null,
1357 suggestions: null,
1358 }),
1359 );
1360 return;
1361 }
1362 if (hasNode(stmt.get('finalizer'))) {
1363 builder.recordError(
1364 new CompilerErrorDetail({
1365 reason: `(BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause`,
1366 category: ErrorCategory.Todo,
1367 loc: stmt.node.loc ?? null,
1368 suggestions: null,
1369 }),
1370 );
1371 }
1372
1373 const handlerBindingPath = handlerPath.get('param');
1374 let handlerBinding: {
1375 place: Place;
1376 path: NodePath<t.Identifier | t.ArrayPattern | t.ObjectPattern>;
1377 } | null = null;
1378 if (hasNode(handlerBindingPath)) {
1379 const place: Place = {
1380 kind: 'Identifier',
1381 identifier: builder.makeTemporary(
1382 handlerBindingPath.node.loc ?? GeneratedSource,
1383 ),
1384 effect: Effect.Unknown,
1385 reactive: false,
1386 loc: handlerBindingPath.node.loc ?? GeneratedSource,
1387 };
1388 promoteTemporary(place.identifier);
1389 lowerValueToTemporary(builder, {
1390 kind: 'DeclareLocal',
1391 lvalue: {
1392 kind: InstructionKind.Catch,
1393 place: {...place},
1394 },
1395 type: null,
1396 loc: handlerBindingPath.node.loc ?? GeneratedSource,
1397 });
1398
1399 handlerBinding = {
1400 path: handlerBindingPath,
1401 place,
1402 };
1403 }
1404
1405 const handler = builder.enter('catch', _blockId => {
1406 if (handlerBinding !== null) {
1407 lowerAssignment(
1408 builder,
1409 handlerBinding.path.node.loc ?? GeneratedSource,
1410 InstructionKind.Catch,
1411 handlerBinding.path,
1412 {...handlerBinding.place},
1413 'Assignment',
1414 );
1415 }
1416 lowerStatement(builder, handlerPath.get('body'));
1417 return {
1418 kind: 'goto',
1419 block: continuationBlock.id,
1420 variant: GotoVariant.Break,
1421 id: makeInstructionId(0),
1422 loc: handlerPath.node.loc ?? GeneratedSource,
1423 };
1424 });
1425
1426 const block = builder.enter('block', _blockId => {
1427 const block = stmt.get('block');
1428 builder.enterTryCatch(handler, () => {
1429 lowerStatement(builder, block);
1430 });
1431 return {
1432 kind: 'goto',
1433 block: continuationBlock.id,
1434 variant: GotoVariant.Try,
1435 id: makeInstructionId(0),
1436 loc: block.node.loc ?? GeneratedSource,
1437 };
1438 });
1439
1440 builder.terminateWithContinuation(
1441 {
1442 kind: 'try',
1443 block,
1444 handlerBinding:
1445 handlerBinding !== null ? {...handlerBinding.place} : null,
1446 handler,
1447 fallthrough: continuationBlock.id,
1448 id: makeInstructionId(0),
1449 loc: stmt.node.loc ?? GeneratedSource,
1450 },
1451 continuationBlock,
1452 );
1453
1454 return;
1455 }
1456 case 'WithStatement': {
1457 builder.recordError(
1458 new CompilerErrorDetail({
1459 reason: `JavaScript 'with' syntax is not supported`,
1460 description: `'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives`,
1461 category: ErrorCategory.UnsupportedSyntax,
1462 loc: stmtPath.node.loc ?? null,
1463 suggestions: null,
1464 }),
1465 );
1466 lowerValueToTemporary(builder, {
1467 kind: 'UnsupportedNode',
1468 loc: stmtPath.node.loc ?? GeneratedSource,
1469 node: stmtPath.node,
1470 });
1471 return;
1472 }
1473 case 'ClassDeclaration': {
1474 /**
1475 * In theory we could support inline class declarations, but this is rare enough in practice
1476 * and complex enough to support that we don't anticipate supporting anytime soon. Developers
1477 * are encouraged to lift classes out of component/hook declarations.
1478 */
1479 builder.recordError(
1480 new CompilerErrorDetail({
1481 reason: 'Inline `class` declarations are not supported',
1482 description: `Move class declarations outside of components/hooks`,
1483 category: ErrorCategory.UnsupportedSyntax,
1484 loc: stmtPath.node.loc ?? null,
1485 suggestions: null,
1486 }),
1487 );
1488 lowerValueToTemporary(builder, {
1489 kind: 'UnsupportedNode',
1490 loc: stmtPath.node.loc ?? GeneratedSource,
1491 node: stmtPath.node,
1492 });
1493 return;
1494 }
1495 case 'EnumDeclaration':
1496 case 'TSEnumDeclaration': {
1497 lowerValueToTemporary(builder, {
1498 kind: 'UnsupportedNode',
1499 loc: stmtPath.node.loc ?? GeneratedSource,
1500 node: stmtPath.node,
1501 });
1502 return;
1503 }
1504 case 'ExportAllDeclaration':
1505 case 'ExportDefaultDeclaration':
1506 case 'ExportNamedDeclaration':
1507 case 'ImportDeclaration':
1508 case 'TSExportAssignment':
1509 case 'TSImportEqualsDeclaration': {
1510 builder.recordError(
1511 new CompilerErrorDetail({
1512 reason:
1513 'JavaScript `import` and `export` statements may only appear at the top level of a module',
1514 category: ErrorCategory.Syntax,
1515 loc: stmtPath.node.loc ?? null,
1516 suggestions: null,
1517 }),
1518 );
1519 lowerValueToTemporary(builder, {
1520 kind: 'UnsupportedNode',
1521 loc: stmtPath.node.loc ?? GeneratedSource,
1522 node: stmtPath.node,
1523 });
1524 return;
1525 }
1526 case 'TSNamespaceExportDeclaration': {
1527 builder.recordError(
1528 new CompilerErrorDetail({
1529 reason:
1530 'TypeScript `namespace` statements may only appear at the top level of a module',
1531 category: ErrorCategory.Syntax,
1532 loc: stmtPath.node.loc ?? null,
1533 suggestions: null,
1534 }),
1535 );
1536 lowerValueToTemporary(builder, {
1537 kind: 'UnsupportedNode',
1538 loc: stmtPath.node.loc ?? GeneratedSource,
1539 node: stmtPath.node,
1540 });
1541 return;
1542 }
1543 case 'DeclareClass':
1544 case 'DeclareExportAllDeclaration':
1545 case 'DeclareExportDeclaration':
1546 case 'DeclareFunction':
1547 case 'DeclareInterface':
1548 case 'DeclareModule':
1549 case 'DeclareModuleExports':
1550 case 'DeclareOpaqueType':
1551 case 'DeclareTypeAlias':
1552 case 'DeclareVariable':
1553 case 'InterfaceDeclaration':
1554 case 'OpaqueType':
1555 case 'TSDeclareFunction':
1556 case 'TSInterfaceDeclaration':
1557 case 'TSModuleDeclaration':
1558 case 'TSTypeAliasDeclaration':
1559 case 'TypeAlias': {
1560 // We do not preserve type annotations/syntax through transformation
1561 return;
1562 }
1563 default: {
1564 return assertExhaustive(
1565 stmtNode,
1566 `Unsupported statement kind '${
1567 (stmtNode as any as NodePath<t.Statement>).type
1568 }'`,
1569 );
1570 }
1571 }
1572 }
1573
1574 function lowerObjectMethod(
1575 builder: HIRBuilder,
1576 property: NodePath<t.ObjectMethod>,
1577 ): InstructionValue {
1578 const loc = property.node.loc ?? GeneratedSource;
1579 const loweredFunc = lowerFunction(builder, property);
1580
1581 return {
1582 kind: 'ObjectMethod',
1583 loc,
1584 loweredFunc,
1585 };
1586 }
1587
1588 function lowerObjectPropertyKey(
1589 builder: HIRBuilder,
1590 property: NodePath<t.ObjectProperty | t.ObjectMethod>,
1591 ): ObjectPropertyKey | null {
1592 const key = property.get('key');
1593 if (key.isStringLiteral()) {
1594 return {
1595 kind: 'string',
1596 name: key.node.value,
1597 };
1598 } else if (property.node.computed && key.isExpression()) {
1599 const place = lowerExpressionToTemporary(builder, key);
1600 return {
1601 kind: 'computed',
1602 name: place,
1603 };
1604 } else if (key.isIdentifier()) {
1605 return {
1606 kind: 'identifier',
1607 name: key.node.name,
1608 };
1609 } else if (key.isNumericLiteral()) {
1610 return {
1611 kind: 'identifier',
1612 name: String(key.node.value),
1613 };
1614 }
1615
1616 builder.recordError(
1617 new CompilerErrorDetail({
1618 reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
1619 category: ErrorCategory.Todo,
1620 loc: key.node.loc ?? null,
1621 suggestions: null,
1622 }),
1623 );
1624 return null;
1625 }
1626
1627 function lowerExpression(
1628 builder: HIRBuilder,
1629 exprPath: NodePath<t.Expression>,
1630 ): InstructionValue {
1631 const exprNode = exprPath.node;
1632 const exprLoc = exprNode.loc ?? GeneratedSource;
1633 switch (exprNode.type) {
1634 case 'Identifier': {
1635 const expr = exprPath as NodePath<t.Identifier>;
1636 const place = lowerIdentifier(builder, expr);
1637 return {
1638 kind: getLoadKind(builder, expr),
1639 place,
1640 loc: exprLoc,
1641 };
1642 }
1643 case 'NullLiteral': {
1644 return {
1645 kind: 'Primitive',
1646 value: null,
1647 loc: exprLoc,
1648 };
1649 }
1650 case 'BooleanLiteral':
1651 case 'NumericLiteral':
1652 case 'StringLiteral': {
1653 const expr = exprPath as NodePath<
1654 t.StringLiteral | t.BooleanLiteral | t.NumericLiteral
1655 >;
1656 const value = expr.node.value;
1657 return {
1658 kind: 'Primitive',
1659 value,
1660 loc: exprLoc,
1661 };
1662 }
1663 case 'ObjectExpression': {
1664 const expr = exprPath as NodePath<t.ObjectExpression>;
1665 const propertyPaths = expr.get('properties');
1666 const properties: Array<ObjectProperty | SpreadPattern> = [];
1667 for (const propertyPath of propertyPaths) {
1668 if (propertyPath.isObjectProperty()) {
1669 const loweredKey = lowerObjectPropertyKey(builder, propertyPath);
1670 if (!loweredKey) {
1671 continue;
1672 }
1673 const valuePath = propertyPath.get('value');
1674 if (!valuePath.isExpression()) {
1675 builder.recordError(
1676 new CompilerErrorDetail({
1677 reason: `(BuildHIR::lowerExpression) Handle ${valuePath.type} values in ObjectExpression`,
1678 category: ErrorCategory.Todo,
1679 loc: valuePath.node.loc ?? null,
1680 suggestions: null,
1681 }),
1682 );
1683 continue;
1684 }
1685 const value = lowerExpressionToTemporary(builder, valuePath);
1686 properties.push({
1687 kind: 'ObjectProperty',
1688 type: 'property',
1689 place: value,
1690 key: loweredKey,
1691 });
1692 } else if (propertyPath.isSpreadElement()) {
1693 const place = lowerExpressionToTemporary(
1694 builder,
1695 propertyPath.get('argument'),
1696 );
1697 properties.push({
1698 kind: 'Spread',
1699 place,
1700 });
1701 } else if (propertyPath.isObjectMethod()) {
1702 if (propertyPath.node.kind !== 'method') {
1703 builder.recordError(
1704 new CompilerErrorDetail({
1705 reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.node.kind} functions in ObjectExpression`,
1706 category: ErrorCategory.Todo,
1707 loc: propertyPath.node.loc ?? null,
1708 suggestions: null,
1709 }),
1710 );
1711 continue;
1712 }
1713 const method = lowerObjectMethod(builder, propertyPath);
1714 const place = lowerValueToTemporary(builder, method);
1715 const loweredKey = lowerObjectPropertyKey(builder, propertyPath);
1716 if (!loweredKey) {
1717 continue;
1718 }
1719 properties.push({
1720 kind: 'ObjectProperty',
1721 type: 'method',
1722 place,
1723 key: loweredKey,
1724 });
1725 } else {
1726 builder.recordError(
1727 new CompilerErrorDetail({
1728 reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.type} properties in ObjectExpression`,
1729 category: ErrorCategory.Todo,
1730 loc: propertyPath.node.loc ?? null,
1731 suggestions: null,
1732 }),
1733 );
1734 continue;
1735 }
1736 }
1737 return {
1738 kind: 'ObjectExpression',
1739 properties,
1740 loc: exprLoc,
1741 };
1742 }
1743 case 'ArrayExpression': {
1744 const expr = exprPath as NodePath<t.ArrayExpression>;
1745 let elements: ArrayExpression['elements'] = [];
1746 for (const element of expr.get('elements')) {
1747 if (element.node == null) {
1748 elements.push({
1749 kind: 'Hole',
1750 });
1751 continue;
1752 } else if (element.isExpression()) {
1753 elements.push(lowerExpressionToTemporary(builder, element));
1754 } else if (element.isSpreadElement()) {
1755 const place = lowerExpressionToTemporary(
1756 builder,
1757 element.get('argument'),
1758 );
1759 elements.push({kind: 'Spread', place});
1760 } else {
1761 builder.recordError(
1762 new CompilerErrorDetail({
1763 reason: `(BuildHIR::lowerExpression) Handle ${element.type} elements in ArrayExpression`,
1764 category: ErrorCategory.Todo,
1765 loc: element.node.loc ?? null,
1766 suggestions: null,
1767 }),
1768 );
1769 continue;
1770 }
1771 }
1772 return {
1773 kind: 'ArrayExpression',
1774 elements,
1775 loc: exprLoc,
1776 };
1777 }
1778 case 'NewExpression': {
1779 const expr = exprPath as NodePath<t.NewExpression>;
1780 const calleePath = expr.get('callee');
1781 if (!calleePath.isExpression()) {
1782 builder.recordError(
1783 new CompilerErrorDetail({
1784 reason: `Expected an expression as the \`new\` expression receiver (v8 intrinsics are not supported)`,
1785 description: `Got a \`${calleePath.node.type}\``,
1786 category: ErrorCategory.Syntax,
1787 loc: calleePath.node.loc ?? null,
1788 suggestions: null,
1789 }),
1790 );
1791 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
1792 }
1793 const callee = lowerExpressionToTemporary(builder, calleePath);
1794 const args = lowerArguments(builder, expr.get('arguments'));
1795
1796 return {
1797 kind: 'NewExpression',
1798 callee,
1799 args,
1800 loc: exprLoc,
1801 };
1802 }
1803 case 'OptionalCallExpression': {
1804 const expr = exprPath as NodePath<t.OptionalCallExpression>;
1805 return lowerOptionalCallExpression(builder, expr, null);
1806 }
1807 case 'CallExpression': {
1808 const expr = exprPath as NodePath<t.CallExpression>;
1809 const calleePath = expr.get('callee');
1810 if (!calleePath.isExpression()) {
1811 builder.recordError(
1812 new CompilerErrorDetail({
1813 reason: `Expected Expression, got ${calleePath.type} in CallExpression (v8 intrinsics not supported). This error is likely caused by a bug in React Compiler. Please file an issue`,
1814 category: ErrorCategory.Todo,
1815 loc: calleePath.node.loc ?? null,
1816 suggestions: null,
1817 }),
1818 );
1819 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
1820 }
1821 if (calleePath.isMemberExpression()) {
1822 const memberExpr = lowerMemberExpression(builder, calleePath);
1823 const propertyPlace = lowerValueToTemporary(builder, memberExpr.value);
1824 const args = lowerArguments(builder, expr.get('arguments'));
1825 return {
1826 kind: 'MethodCall',
1827 receiver: memberExpr.object,
1828 property: {...propertyPlace},
1829 args,
1830 loc: exprLoc,
1831 };
1832 } else {
1833 const callee = lowerExpressionToTemporary(builder, calleePath);
1834 const args = lowerArguments(builder, expr.get('arguments'));
1835 return {
1836 kind: 'CallExpression',
1837 callee,
1838 args,
1839 loc: exprLoc,
1840 };
1841 }
1842 }
1843 case 'BinaryExpression': {
1844 const expr = exprPath as NodePath<t.BinaryExpression>;
1845 const leftPath = expr.get('left');
1846 if (!leftPath.isExpression()) {
1847 builder.recordError(
1848 new CompilerErrorDetail({
1849 reason: `(BuildHIR::lowerExpression) Expected Expression, got ${leftPath.type} lval in BinaryExpression`,
1850 category: ErrorCategory.Todo,
1851 loc: leftPath.node.loc ?? null,
1852 suggestions: null,
1853 }),
1854 );
1855 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
1856 }
1857 const left = lowerExpressionToTemporary(builder, leftPath);
1858 const right = lowerExpressionToTemporary(builder, expr.get('right'));
1859 const operator = expr.node.operator;
1860 if (operator === '|>') {
1861 builder.recordError(
1862 new CompilerErrorDetail({
1863 reason: `(BuildHIR::lowerExpression) Pipe operator not supported`,
1864 category: ErrorCategory.Todo,
1865 loc: leftPath.node.loc ?? null,
1866 suggestions: null,
1867 }),
1868 );
1869 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
1870 }
1871 return {
1872 kind: 'BinaryExpression',
1873 operator,
1874 left,
1875 right,
1876 loc: exprLoc,
1877 };
1878 }
1879 case 'SequenceExpression': {
1880 const expr = exprPath as NodePath<t.SequenceExpression>;
1881 const exprLoc = expr.node.loc ?? GeneratedSource;
1882
1883 const continuationBlock = builder.reserve(builder.currentBlockKind());
1884 const place = buildTemporaryPlace(builder, exprLoc);
1885
1886 const sequenceBlock = builder.enter('sequence', _ => {
1887 let last: Place | null = null;
1888 for (const item of expr.get('expressions')) {
1889 last = lowerExpressionToTemporary(builder, item);
1890 }
1891 if (last === null) {
1892 builder.recordError(
1893 new CompilerErrorDetail({
1894 reason: `Expected sequence expression to have at least one expression`,
1895 category: ErrorCategory.Syntax,
1896 loc: expr.node.loc ?? null,
1897 suggestions: null,
1898 }),
1899 );
1900 } else {
1901 lowerValueToTemporary(builder, {
1902 kind: 'StoreLocal',
1903 lvalue: {kind: InstructionKind.Const, place: {...place}},
1904 value: last,
1905 type: null,
1906 loc: exprLoc,
1907 });
1908 }
1909 return {
1910 kind: 'goto',
1911 id: makeInstructionId(0),
1912 block: continuationBlock.id,
1913 loc: exprLoc,
1914 variant: GotoVariant.Break,
1915 };
1916 });
1917
1918 builder.terminateWithContinuation(
1919 {
1920 kind: 'sequence',
1921 block: sequenceBlock,
1922 fallthrough: continuationBlock.id,
1923 id: makeInstructionId(0),
1924 loc: exprLoc,
1925 },
1926 continuationBlock,
1927 );
1928 return {kind: 'LoadLocal', place, loc: place.loc};
1929 }
1930 case 'ConditionalExpression': {
1931 const expr = exprPath as NodePath<t.ConditionalExpression>;
1932 const exprLoc = expr.node.loc ?? GeneratedSource;
1933
1934 // Block for code following the if
1935 const continuationBlock = builder.reserve(builder.currentBlockKind());
1936 const testBlock = builder.reserve('value');
1937 const place = buildTemporaryPlace(builder, exprLoc);
1938
1939 // Block for the consequent (if the test is truthy)
1940 const consequentBlock = builder.enter('value', _blockId => {
1941 const consequentPath = expr.get('consequent');
1942 const consequent = lowerExpressionToTemporary(builder, consequentPath);
1943 lowerValueToTemporary(builder, {
1944 kind: 'StoreLocal',
1945 lvalue: {kind: InstructionKind.Const, place: {...place}},
1946 value: consequent,
1947 type: null,
1948 loc: exprLoc,
1949 });
1950 return {
1951 kind: 'goto',
1952 block: continuationBlock.id,
1953 variant: GotoVariant.Break,
1954 id: makeInstructionId(0),
1955 loc: consequentPath.node.loc ?? GeneratedSource,
1956 };
1957 });
1958 // Block for the alternate (if the test is not truthy)
1959 const alternateBlock = builder.enter('value', _blockId => {
1960 const alternatePath = expr.get('alternate');
1961 const alternate = lowerExpressionToTemporary(builder, alternatePath);
1962 lowerValueToTemporary(builder, {
1963 kind: 'StoreLocal',
1964 lvalue: {kind: InstructionKind.Const, place: {...place}},
1965 value: alternate,
1966 type: null,
1967 loc: exprLoc,
1968 });
1969 return {
1970 kind: 'goto',
1971 block: continuationBlock.id,
1972 variant: GotoVariant.Break,
1973 id: makeInstructionId(0),
1974 loc: alternatePath.node.loc ?? GeneratedSource,
1975 };
1976 });
1977
1978 builder.terminateWithContinuation(
1979 {
1980 kind: 'ternary',
1981 fallthrough: continuationBlock.id,
1982 id: makeInstructionId(0),
1983 test: testBlock.id,
1984 loc: exprLoc,
1985 },
1986 testBlock,
1987 );
1988 const testPlace = lowerExpressionToTemporary(builder, expr.get('test'));
1989 builder.terminateWithContinuation(
1990 {
1991 kind: 'branch',
1992 test: {...testPlace},
1993 consequent: consequentBlock,
1994 alternate: alternateBlock,
1995 fallthrough: continuationBlock.id,
1996 id: makeInstructionId(0),
1997 loc: exprLoc,
1998 },
1999 continuationBlock,
2000 );
2001 return {kind: 'LoadLocal', place, loc: place.loc};
2002 }
2003 case 'LogicalExpression': {
2004 const expr = exprPath as NodePath<t.LogicalExpression>;
2005 const exprLoc = expr.node.loc ?? GeneratedSource;
2006 const continuationBlock = builder.reserve(builder.currentBlockKind());
2007 const testBlock = builder.reserve('value');
2008 const place = buildTemporaryPlace(builder, exprLoc);
2009 const leftPlace = buildTemporaryPlace(
2010 builder,
2011 expr.get('left').node.loc ?? GeneratedSource,
2012 );
2013 const consequent = builder.enter('value', () => {
2014 lowerValueToTemporary(builder, {
2015 kind: 'StoreLocal',
2016 lvalue: {kind: InstructionKind.Const, place: {...place}},
2017 value: {...leftPlace},
2018 type: null,
2019 loc: leftPlace.loc,
2020 });
2021 return {
2022 kind: 'goto',
2023 block: continuationBlock.id,
2024 variant: GotoVariant.Break,
2025 id: makeInstructionId(0),
2026 loc: leftPlace.loc,
2027 };
2028 });
2029 const alternate = builder.enter('value', () => {
2030 const right = lowerExpressionToTemporary(builder, expr.get('right'));
2031 lowerValueToTemporary(builder, {
2032 kind: 'StoreLocal',
2033 lvalue: {kind: InstructionKind.Const, place: {...place}},
2034 value: {...right},
2035 type: null,
2036 loc: right.loc,
2037 });
2038 return {
2039 kind: 'goto',
2040 block: continuationBlock.id,
2041 variant: GotoVariant.Break,
2042 id: makeInstructionId(0),
2043 loc: right.loc,
2044 };
2045 });
2046 builder.terminateWithContinuation(
2047 {
2048 kind: 'logical',
2049 fallthrough: continuationBlock.id,
2050 id: makeInstructionId(0),
2051 test: testBlock.id,
2052 operator: expr.node.operator,
2053 loc: exprLoc,
2054 },
2055 testBlock,
2056 );
2057 const leftValue = lowerExpressionToTemporary(builder, expr.get('left'));
2058 builder.push({
2059 id: makeInstructionId(0),
2060 lvalue: {...leftPlace},
2061 value: {
2062 kind: 'LoadLocal',
2063 place: leftValue,
2064 loc: exprLoc,
2065 },
2066 effects: null,
2067 loc: exprLoc,
2068 });
2069 builder.terminateWithContinuation(
2070 {
2071 kind: 'branch',
2072 test: {...leftPlace},
2073 consequent,
2074 alternate,
2075 fallthrough: continuationBlock.id,
2076 id: makeInstructionId(0),
2077 loc: exprLoc,
2078 },
2079 continuationBlock,
2080 );
2081 return {kind: 'LoadLocal', place, loc: place.loc};
2082 }
2083 case 'AssignmentExpression': {
2084 const expr = exprPath as NodePath<t.AssignmentExpression>;
2085 const operator = expr.node.operator;
2086
2087 if (operator === '=') {
2088 const left = expr.get('left');
2089 if (left.isLVal()) {
2090 return lowerAssignment(
2091 builder,
2092 left.node.loc ?? GeneratedSource,
2093 InstructionKind.Reassign,
2094 left,
2095 lowerExpressionToTemporary(builder, expr.get('right')),
2096 left.isArrayPattern() || left.isObjectPattern()
2097 ? 'Destructure'
2098 : 'Assignment',
2099 );
2100 } else {
2101 /**
2102 * OptionalMemberExpressions as the left side of an AssignmentExpression are Stage 1 and
2103 * not supported by React Compiler yet.
2104 */
2105 builder.recordError(
2106 new CompilerErrorDetail({
2107 reason: `(BuildHIR::lowerExpression) Unsupported syntax on the left side of an AssignmentExpression`,
2108 description: `Expected an LVal, got: ${left.type}`,
2109 category: ErrorCategory.Todo,
2110 loc: left.node.loc ?? null,
2111 suggestions: null,
2112 }),
2113 );
2114 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
2115 }
2116 }
2117
2118 const operators: {
2119 [key: string]: Exclude<t.BinaryExpression['operator'], '|>'>;
2120 } = {
2121 '+=': '+',
2122 '-=': '-',
2123 '/=': '/',
2124 '%=': '%',
2125 '*=': '*',
2126 '**=': '**',
2127 '&=': '&',
2128 '|=': '|',
2129 '>>=': '>>',
2130 '>>>=': '>>>',
2131 '<<=': '<<',
2132 '^=': '^',
2133 };
2134 const binaryOperator = operators[operator];
2135 if (binaryOperator == null) {
2136 builder.recordError(
2137 new CompilerErrorDetail({
2138 reason: `(BuildHIR::lowerExpression) Handle ${operator} operators in AssignmentExpression`,
2139 category: ErrorCategory.Todo,
2140 loc: expr.node.loc ?? null,
2141 suggestions: null,
2142 }),
2143 );
2144 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
2145 }
2146 const left = expr.get('left');
2147 const leftNode = left.node;
2148 switch (leftNode.type) {
2149 case 'Identifier': {
2150 const leftExpr = left as NodePath<t.Identifier>;
2151 const leftPlace = lowerExpressionToTemporary(builder, leftExpr);
2152 const right = lowerExpressionToTemporary(builder, expr.get('right'));
2153 const binaryPlace = lowerValueToTemporary(builder, {
2154 kind: 'BinaryExpression',
2155 operator: binaryOperator,
2156 left: leftPlace,
2157 right,
2158 loc: exprLoc,
2159 });
2160 const binding = builder.resolveIdentifier(leftExpr);
2161 if (binding.kind === 'Identifier') {
2162 const identifier = lowerIdentifier(builder, leftExpr);
2163 const kind = getStoreKind(builder, leftExpr);
2164 if (kind === 'StoreLocal') {
2165 lowerValueToTemporary(builder, {
2166 kind: 'StoreLocal',
2167 lvalue: {
2168 place: {...identifier},
2169 kind: InstructionKind.Reassign,
2170 },
2171 value: {...binaryPlace},
2172 type: null,
2173 loc: exprLoc,
2174 });
2175 return {kind: 'LoadLocal', place: identifier, loc: exprLoc};
2176 } else {
2177 lowerValueToTemporary(builder, {
2178 kind: 'StoreContext',
2179 lvalue: {
2180 place: {...identifier},
2181 kind: InstructionKind.Reassign,
2182 },
2183 value: {...binaryPlace},
2184 loc: exprLoc,
2185 });
2186 return {kind: 'LoadContext', place: identifier, loc: exprLoc};
2187 }
2188 } else {
2189 const temporary = lowerValueToTemporary(builder, {
2190 kind: 'StoreGlobal',
2191 name: leftExpr.node.name,
2192 value: {...binaryPlace},
2193 loc: exprLoc,
2194 });
2195 return {kind: 'LoadLocal', place: temporary, loc: temporary.loc};
2196 }
2197 }
2198 case 'MemberExpression': {
2199 // a.b.c += <right>
2200 const leftExpr = left as NodePath<t.MemberExpression>;
2201 const {object, property, value} = lowerMemberExpression(
2202 builder,
2203 leftExpr,
2204 );
2205
2206 // Store the previous value to a temporary
2207 const previousValuePlace = lowerValueToTemporary(builder, value);
2208 // Store the new value to a temporary
2209 const newValuePlace = lowerValueToTemporary(builder, {
2210 kind: 'BinaryExpression',
2211 operator: binaryOperator,
2212 left: {...previousValuePlace},
2213 right: lowerExpressionToTemporary(builder, expr.get('right')),
2214 loc: leftExpr.node.loc ?? GeneratedSource,
2215 });
2216
2217 // Save the result back to the property
2218 if (typeof property === 'string' || typeof property === 'number') {
2219 return {
2220 kind: 'PropertyStore',
2221 object: {...object},
2222 property: makePropertyLiteral(property),
2223 value: {...newValuePlace},
2224 loc: leftExpr.node.loc ?? GeneratedSource,
2225 };
2226 } else {
2227 return {
2228 kind: 'ComputedStore',
2229 object: {...object},
2230 property: {...property},
2231 value: {...newValuePlace},
2232 loc: leftExpr.node.loc ?? GeneratedSource,
2233 };
2234 }
2235 }
2236 default: {
2237 builder.recordError(
2238 new CompilerErrorDetail({
2239 reason: `(BuildHIR::lowerExpression) Expected Identifier or MemberExpression, got ${expr.type} lval in AssignmentExpression`,
2240 category: ErrorCategory.Todo,
2241 loc: expr.node.loc ?? null,
2242 suggestions: null,
2243 }),
2244 );
2245 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
2246 }
2247 }
2248 }
2249 case 'OptionalMemberExpression': {
2250 const expr = exprPath as NodePath<t.OptionalMemberExpression>;
2251 const {value} = lowerOptionalMemberExpression(builder, expr, null);
2252 return {kind: 'LoadLocal', place: value, loc: value.loc};
2253 }
2254 case 'MemberExpression': {
2255 const expr = exprPath as NodePath<
2256 t.MemberExpression | t.OptionalMemberExpression
2257 >;
2258 const {value} = lowerMemberExpression(builder, expr);
2259 const place = lowerValueToTemporary(builder, value);
2260 return {kind: 'LoadLocal', place, loc: place.loc};
2261 }
2262 case 'JSXElement': {
2263 const expr = exprPath as NodePath<t.JSXElement>;
2264 const opening = expr.get('openingElement');
2265 const openingLoc = opening.node.loc ?? GeneratedSource;
2266 const tag = lowerJsxElementName(builder, opening.get('name'));
2267 const props: Array<JsxAttribute> = [];
2268 for (const attribute of opening.get('attributes')) {
2269 if (attribute.isJSXSpreadAttribute()) {
2270 const argument = lowerExpressionToTemporary(
2271 builder,
2272 attribute.get('argument'),
2273 );
2274 props.push({kind: 'JsxSpreadAttribute', argument});
2275 continue;
2276 }
2277 if (!attribute.isJSXAttribute()) {
2278 builder.recordError(
2279 new CompilerErrorDetail({
2280 reason: `(BuildHIR::lowerExpression) Handle ${attribute.type} attributes in JSXElement`,
2281 category: ErrorCategory.Todo,
2282 loc: attribute.node.loc ?? null,
2283 suggestions: null,
2284 }),
2285 );
2286 continue;
2287 }
2288 const namePath = attribute.get('name');
2289 let propName;
2290 if (namePath.isJSXIdentifier()) {
2291 propName = namePath.node.name;
2292 if (propName.indexOf(':') !== -1) {
2293 builder.recordError(
2294 new CompilerErrorDetail({
2295 reason: `(BuildHIR::lowerExpression) Unexpected colon in attribute name \`${propName}\``,
2296 category: ErrorCategory.Todo,
2297 loc: namePath.node.loc ?? null,
2298 suggestions: null,
2299 }),
2300 );
2301 }
2302 } else {
2303 CompilerError.invariant(namePath.isJSXNamespacedName(), {
2304 reason: 'Refinement',
2305 loc: namePath.node.loc ?? GeneratedSource,
2306 });
2307 const namespace = namePath.node.namespace.name;
2308 const name = namePath.node.name.name;
2309 propName = `${namespace}:${name}`;
2310 }
2311 const valueExpr = attribute.get('value');
2312 let value;
2313 if (valueExpr.isJSXElement() || valueExpr.isStringLiteral()) {
2314 value = lowerExpressionToTemporary(builder, valueExpr);
2315 } else if (valueExpr.type == null) {
2316 value = lowerValueToTemporary(builder, {
2317 kind: 'Primitive',
2318 value: true,
2319 loc: attribute.node.loc ?? GeneratedSource,
2320 });
2321 } else {
2322 if (!valueExpr.isJSXExpressionContainer()) {
2323 builder.recordError(
2324 new CompilerErrorDetail({
2325 reason: `(BuildHIR::lowerExpression) Handle ${valueExpr.type} attribute values in JSXElement`,
2326 category: ErrorCategory.Todo,
2327 loc: valueExpr.node?.loc ?? null,
2328 suggestions: null,
2329 }),
2330 );
2331 continue;
2332 }
2333 const expression = valueExpr.get('expression');
2334 if (!expression.isExpression()) {
2335 builder.recordError(
2336 new CompilerErrorDetail({
2337 reason: `(BuildHIR::lowerExpression) Handle ${expression.type} expressions in JSXExpressionContainer within JSXElement`,
2338 category: ErrorCategory.Todo,
2339 loc: valueExpr.node.loc ?? null,
2340 suggestions: null,
2341 }),
2342 );
2343 continue;
2344 }
2345 value = lowerExpressionToTemporary(builder, expression);
2346 }
2347 props.push({kind: 'JsxAttribute', name: propName, place: value});
2348 }
2349
2350 const isFbt =
2351 tag.kind === 'BuiltinTag' && (tag.name === 'fbt' || tag.name === 'fbs');
2352 if (isFbt) {
2353 const tagName = tag.name;
2354 const openingIdentifier = opening.get('name');
2355 const tagIdentifier = openingIdentifier.isJSXIdentifier()
2356 ? builder.resolveIdentifier(openingIdentifier)
2357 : null;
2358 if (tagIdentifier != null) {
2359 // This is already checked in builder.resolveIdentifier
2360 CompilerError.invariant(tagIdentifier.kind !== 'Identifier', {
2361 reason: `<${tagName}> tags should be module-level imports`,
2362 loc: openingIdentifier.node.loc ?? GeneratedSource,
2363 });
2364 }
2365 // see `error.todo-multiple-fbt-plural` fixture for explanation
2366 const fbtLocations = {
2367 enum: new Array<SourceLocation>(),
2368 plural: new Array<SourceLocation>(),
2369 pronoun: new Array<SourceLocation>(),
2370 };
2371 expr.traverse({
2372 JSXClosingElement(path) {
2373 path.skip();
2374 },
2375 JSXNamespacedName(path) {
2376 if (path.node.namespace.name === tagName) {
2377 switch (path.node.name.name) {
2378 case 'enum':
2379 fbtLocations.enum.push(path.node.loc ?? GeneratedSource);
2380 break;
2381 case 'plural':
2382 fbtLocations.plural.push(path.node.loc ?? GeneratedSource);
2383 break;
2384 case 'pronoun':
2385 fbtLocations.pronoun.push(path.node.loc ?? GeneratedSource);
2386 break;
2387 }
2388 }
2389 },
2390 });
2391 for (const [name, locations] of Object.entries(fbtLocations)) {
2392 if (locations.length > 1) {
2393 builder.recordError(
2394 new CompilerDiagnostic({
2395 category: ErrorCategory.Todo,
2396 reason: 'Support duplicate fbt tags',
2397 description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
2398 details: locations.map(loc => {
2399 return {
2400 kind: 'error' as const,
2401 message: `Multiple \`<${tagName}:${name}>\` tags found`,
2402 loc,
2403 };
2404 }),
2405 }),
2406 );
2407 }
2408 }
2409 }
2410
2411 /**
2412 * Increment fbt counter before traversing into children, as whitespace
2413 * in jsx text is handled differently for fbt subtrees.
2414 */
2415 isFbt && builder.fbtDepth++;
2416 const children: Array<Place> = expr
2417 .get('children')
2418 .map(child => lowerJsxElement(builder, child))
2419 .filter(notNull);
2420 isFbt && builder.fbtDepth--;
2421
2422 return {
2423 kind: 'JsxExpression',
2424 tag,
2425 props,
2426 children: children.length === 0 ? null : children,
2427 loc: exprLoc,
2428 openingLoc: openingLoc,
2429 closingLoc: expr.get('closingElement').node?.loc ?? GeneratedSource,
2430 };
2431 }
2432 case 'JSXFragment': {
2433 const expr = exprPath as NodePath<t.JSXFragment>;
2434 const children: Array<Place> = expr
2435 .get('children')
2436 .map(child => lowerJsxElement(builder, child))
2437 .filter(notNull);
2438 return {
2439 kind: 'JsxFragment',
2440 children,
2441 loc: exprLoc,
2442 };
2443 }
2444 case 'ArrowFunctionExpression':
2445 case 'FunctionExpression': {
2446 const expr = exprPath as NodePath<
2447 t.FunctionExpression | t.ArrowFunctionExpression
2448 >;
2449 return lowerFunctionToValue(builder, expr);
2450 }
2451 case 'TaggedTemplateExpression': {
2452 const expr = exprPath as NodePath<t.TaggedTemplateExpression>;
2453 if (expr.get('quasi').get('expressions').length !== 0) {
2454 builder.recordError(
2455 new CompilerErrorDetail({
2456 reason:
2457 '(BuildHIR::lowerExpression) Handle tagged template with interpolations',
2458 category: ErrorCategory.Todo,
2459 loc: exprPath.node.loc ?? null,
2460 suggestions: null,
2461 }),
2462 );
2463 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
2464 }
2465 CompilerError.invariant(expr.get('quasi').get('quasis').length == 1, {
2466 reason:
2467 "there should be only one quasi as we don't support interpolations yet",
2468 loc: expr.node.loc ?? GeneratedSource,
2469 });
2470 const value = expr.get('quasi').get('quasis').at(0)!.node.value;
2471 if (value.raw !== value.cooked) {
2472 builder.recordError(
2473 new CompilerErrorDetail({
2474 reason:
2475 '(BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value',
2476 category: ErrorCategory.Todo,
2477 loc: exprPath.node.loc ?? null,
2478 suggestions: null,
2479 }),
2480 );
2481 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
2482 }
2483
2484 return {
2485 kind: 'TaggedTemplateExpression',
2486 tag: lowerExpressionToTemporary(builder, expr.get('tag')),
2487 value,
2488 loc: exprLoc,
2489 };
2490 }
2491 case 'TemplateLiteral': {
2492 const expr = exprPath as NodePath<t.TemplateLiteral>;
2493 const subexprs = expr.get('expressions');
2494 const quasis = expr.get('quasis');
2495
2496 if (subexprs.length !== quasis.length - 1) {
2497 builder.recordError(
2498 new CompilerErrorDetail({
2499 reason: `Unexpected quasi and subexpression lengths in template literal`,
2500 category: ErrorCategory.Syntax,
2501 loc: exprPath.node.loc ?? null,
2502 suggestions: null,
2503 }),
2504 );
2505 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
2506 }
2507
2508 if (subexprs.some(e => !e.isExpression())) {
2509 builder.recordError(
2510 new CompilerErrorDetail({
2511 reason: `(BuildHIR::lowerAssignment) Handle TSType in TemplateLiteral.`,
2512 category: ErrorCategory.Todo,
2513 loc: exprPath.node.loc ?? null,
2514 suggestions: null,
2515 }),
2516 );
2517 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
2518 }
2519
2520 const subexprPlaces = subexprs.map(e =>
2521 lowerExpressionToTemporary(builder, e as NodePath<t.Expression>),
2522 );
2523
2524 return {
2525 kind: 'TemplateLiteral',
2526 subexprs: subexprPlaces,
2527 quasis: expr.get('quasis').map(q => q.node.value),
2528 loc: exprLoc,
2529 };
2530 }
2531 case 'UnaryExpression': {
2532 let expr = exprPath as NodePath<t.UnaryExpression>;
2533 if (expr.node.operator === 'delete') {
2534 const argument = expr.get('argument');
2535 if (argument.isMemberExpression()) {
2536 const {object, property} = lowerMemberExpression(builder, argument);
2537 if (typeof property === 'string' || typeof property === 'number') {
2538 return {
2539 kind: 'PropertyDelete',
2540 object,
2541 property: makePropertyLiteral(property),
2542 loc: exprLoc,
2543 };
2544 } else {
2545 return {
2546 kind: 'ComputedDelete',
2547 object,
2548 property,
2549 loc: exprLoc,
2550 };
2551 }
2552 } else {
2553 builder.recordError(
2554 new CompilerErrorDetail({
2555 reason: `Only object properties can be deleted`,
2556 category: ErrorCategory.Syntax,
2557 loc: expr.node.loc ?? null,
2558 suggestions: [
2559 {
2560 description: 'Remove this line',
2561 range: [expr.node.start!, expr.node.end!],
2562 op: CompilerSuggestionOperation.Remove,
2563 },
2564 ],
2565 }),
2566 );
2567 return {kind: 'UnsupportedNode', node: expr.node, loc: exprLoc};
2568 }
2569 } else if (expr.node.operator === 'throw') {
2570 builder.recordError(
2571 new CompilerErrorDetail({
2572 reason: `Throw expressions are not supported`,
2573 category: ErrorCategory.Syntax,
2574 loc: expr.node.loc ?? null,
2575 suggestions: [
2576 {
2577 description: 'Remove this line',
2578 range: [expr.node.start!, expr.node.end!],
2579 op: CompilerSuggestionOperation.Remove,
2580 },
2581 ],
2582 }),
2583 );
2584 return {kind: 'UnsupportedNode', node: expr.node, loc: exprLoc};
2585 } else {
2586 return {
2587 kind: 'UnaryExpression',
2588 operator: expr.node.operator,
2589 value: lowerExpressionToTemporary(builder, expr.get('argument')),
2590 loc: exprLoc,
2591 };
2592 }
2593 }
2594 case 'AwaitExpression': {
2595 let expr = exprPath as NodePath<t.AwaitExpression>;
2596 return {
2597 kind: 'Await',
2598 value: lowerExpressionToTemporary(builder, expr.get('argument')),
2599 loc: exprLoc,
2600 };
2601 }
2602 case 'TypeCastExpression': {
2603 let expr = exprPath as NodePath<t.TypeCastExpression>;
2604 const typeAnnotation = expr.get('typeAnnotation').get('typeAnnotation');
2605 return {
2606 kind: 'TypeCastExpression',
2607 value: lowerExpressionToTemporary(builder, expr.get('expression')),
2608 typeAnnotation: typeAnnotation.node,
2609 typeAnnotationKind: 'cast',
2610 type: lowerType(typeAnnotation.node),
2611 loc: exprLoc,
2612 };
2613 }
2614 case 'TSSatisfiesExpression': {
2615 let expr = exprPath as NodePath<t.TSSatisfiesExpression>;
2616 const typeAnnotation = expr.get('typeAnnotation');
2617 return {
2618 kind: 'TypeCastExpression',
2619 value: lowerExpressionToTemporary(builder, expr.get('expression')),
2620 typeAnnotation: typeAnnotation.node,
2621 typeAnnotationKind: 'satisfies',
2622 type: lowerType(typeAnnotation.node),
2623 loc: exprLoc,
2624 };
2625 }
2626 case 'TSAsExpression': {
2627 let expr = exprPath as NodePath<t.TSAsExpression>;
2628 const typeAnnotation = expr.get('typeAnnotation');
2629 return {
2630 kind: 'TypeCastExpression',
2631 value: lowerExpressionToTemporary(builder, expr.get('expression')),
2632 typeAnnotation: typeAnnotation.node,
2633 typeAnnotationKind: 'as',
2634 type: lowerType(typeAnnotation.node),
2635 loc: exprLoc,
2636 };
2637 }
2638 case 'UpdateExpression': {
2639 let expr = exprPath as NodePath<t.UpdateExpression>;
2640 const argument = expr.get('argument');
2641 if (argument.isMemberExpression()) {
2642 const binaryOperator = expr.node.operator === '++' ? '+' : '-';
2643 const leftExpr = argument as NodePath<t.MemberExpression>;
2644 const {object, property, value} = lowerMemberExpression(
2645 builder,
2646 leftExpr,
2647 );
2648
2649 // Store the previous value to a temporary
2650 const previousValuePlace = lowerValueToTemporary(builder, value);
2651 // Store the new value to a temporary
2652 const updatedValue = lowerValueToTemporary(builder, {
2653 kind: 'BinaryExpression',
2654 operator: binaryOperator,
2655 left: {...previousValuePlace},
2656 right: lowerValueToTemporary(builder, {
2657 kind: 'Primitive',
2658 value: 1,
2659 loc: GeneratedSource,
2660 }),
2661 loc: leftExpr.node.loc ?? GeneratedSource,
2662 });
2663
2664 // Save the result back to the property
2665 let newValuePlace;
2666 if (typeof property === 'string' || typeof property === 'number') {
2667 newValuePlace = lowerValueToTemporary(builder, {
2668 kind: 'PropertyStore',
2669 object: {...object},
2670 property: makePropertyLiteral(property),
2671 value: {...updatedValue},
2672 loc: leftExpr.node.loc ?? GeneratedSource,
2673 });
2674 } else {
2675 newValuePlace = lowerValueToTemporary(builder, {
2676 kind: 'ComputedStore',
2677 object: {...object},
2678 property: {...property},
2679 value: {...updatedValue},
2680 loc: leftExpr.node.loc ?? GeneratedSource,
2681 });
2682 }
2683
2684 return {
2685 kind: 'LoadLocal',
2686 place: expr.node.prefix
2687 ? {...newValuePlace}
2688 : {...previousValuePlace},
2689 loc: exprLoc,
2690 };
2691 }
2692 if (!argument.isIdentifier()) {
2693 builder.recordError(
2694 new CompilerErrorDetail({
2695 reason: `(BuildHIR::lowerExpression) Handle UpdateExpression with ${argument.type} argument`,
2696 category: ErrorCategory.Todo,
2697 loc: exprPath.node.loc ?? null,
2698 suggestions: null,
2699 }),
2700 );
2701 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
2702 } else if (builder.isContextIdentifier(argument)) {
2703 builder.recordError(
2704 new CompilerErrorDetail({
2705 reason: `(BuildHIR::lowerExpression) Handle UpdateExpression to variables captured within lambdas.`,
2706 category: ErrorCategory.Todo,
2707 loc: exprPath.node.loc ?? null,
2708 suggestions: null,
2709 }),
2710 );
2711 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
2712 }
2713 const lvalue = lowerIdentifierForAssignment(
2714 builder,
2715 argument.node.loc ?? GeneratedSource,
2716 InstructionKind.Reassign,
2717 argument,
2718 );
2719 if (lvalue === null) {
2720 /*
2721 * lowerIdentifierForAssignment should have already reported an error if it returned null,
2722 * we check here just in case
2723 */
2724 if (!builder.environment.hasErrors()) {
2725 builder.recordError(
2726 new CompilerErrorDetail({
2727 reason: `(BuildHIR::lowerExpression) Found an invalid UpdateExpression without a previously reported error`,
2728 category: ErrorCategory.Invariant,
2729 loc: exprLoc,
2730 suggestions: null,
2731 }),
2732 );
2733 }
2734 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
2735 } else if (lvalue.kind === 'Global') {
2736 builder.recordError(
2737 new CompilerErrorDetail({
2738 reason: `(BuildHIR::lowerExpression) Support UpdateExpression where argument is a global`,
2739 category: ErrorCategory.Todo,
2740 loc: exprLoc,
2741 suggestions: null,
2742 }),
2743 );
2744 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
2745 }
2746 const value = lowerIdentifier(builder, argument);
2747 if (expr.node.prefix) {
2748 return {
2749 kind: 'PrefixUpdate',
2750 lvalue,
2751 operation: expr.node.operator,
2752 value,
2753 loc: exprLoc,
2754 };
2755 } else {
2756 return {
2757 kind: 'PostfixUpdate',
2758 lvalue,
2759 operation: expr.node.operator,
2760 value,
2761 loc: exprLoc,
2762 };
2763 }
2764 }
2765 case 'RegExpLiteral': {
2766 let expr = exprPath as NodePath<t.RegExpLiteral>;
2767 return {
2768 kind: 'RegExpLiteral',
2769 pattern: expr.node.pattern,
2770 flags: expr.node.flags,
2771 loc: expr.node.loc ?? GeneratedSource,
2772 };
2773 }
2774 case 'TSInstantiationExpression':
2775 case 'TSNonNullExpression': {
2776 let expr = exprPath as NodePath<t.TSNonNullExpression>;
2777 return lowerExpression(builder, expr.get('expression'));
2778 }
2779 case 'MetaProperty': {
2780 let expr = exprPath as NodePath<t.MetaProperty>;
2781 if (
2782 expr.node.meta.name === 'import' &&
2783 expr.node.property.name === 'meta'
2784 ) {
2785 return {
2786 kind: 'MetaProperty',
2787 meta: expr.node.meta.name,
2788 property: expr.node.property.name,
2789 loc: expr.node.loc ?? GeneratedSource,
2790 };
2791 }
2792
2793 builder.recordError(
2794 new CompilerErrorDetail({
2795 reason: `(BuildHIR::lowerExpression) Handle MetaProperty expressions other than import.meta`,
2796 category: ErrorCategory.Todo,
2797 loc: exprPath.node.loc ?? null,
2798 suggestions: null,
2799 }),
2800 );
2801 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
2802 }
2803 default: {
2804 builder.recordError(
2805 new CompilerErrorDetail({
2806 reason: `(BuildHIR::lowerExpression) Handle ${exprPath.type} expressions`,
2807 category: ErrorCategory.Todo,
2808 loc: exprPath.node.loc ?? null,
2809 suggestions: null,
2810 }),
2811 );
2812 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
2813 }
2814 }
2815 }
2816
2817 function lowerOptionalMemberExpression(
2818 builder: HIRBuilder,
2819 expr: NodePath<t.OptionalMemberExpression>,
2820 parentAlternate: BlockId | null,
2821 ): {object: Place; value: Place} {
2822 const optional = expr.node.optional;
2823 const loc = expr.node.loc ?? GeneratedSource;
2824 const place = buildTemporaryPlace(builder, loc);
2825 const continuationBlock = builder.reserve(builder.currentBlockKind());
2826 const consequent = builder.reserve('value');
2827
2828 /*
2829 * block to evaluate if the callee is null/undefined, this sets the result of the call to undefined.
2830 * note that we only create an alternate when first entering an optional subtree of the ast: if this
2831 * is a child of an optional node, we use the alterate created by the parent.
2832 */
2833 const alternate =
2834 parentAlternate !== null
2835 ? parentAlternate
2836 : builder.enter('value', () => {
2837 const temp = lowerValueToTemporary(builder, {
2838 kind: 'Primitive',
2839 value: undefined,
2840 loc,
2841 });
2842 lowerValueToTemporary(builder, {
2843 kind: 'StoreLocal',
2844 lvalue: {kind: InstructionKind.Const, place: {...place}},
2845 value: {...temp},
2846 type: null,
2847 loc,
2848 });
2849 return {
2850 kind: 'goto',
2851 variant: GotoVariant.Break,
2852 block: continuationBlock.id,
2853 id: makeInstructionId(0),
2854 loc,
2855 };
2856 });
2857
2858 let object: Place | null = null;
2859 const testBlock = builder.enter('value', () => {
2860 const objectPath = expr.get('object');
2861 if (objectPath.isOptionalMemberExpression()) {
2862 const {value} = lowerOptionalMemberExpression(
2863 builder,
2864 objectPath,
2865 alternate,
2866 );
2867 object = value;
2868 } else if (objectPath.isOptionalCallExpression()) {
2869 const value = lowerOptionalCallExpression(builder, objectPath, alternate);
2870 object = lowerValueToTemporary(builder, value);
2871 } else {
2872 object = lowerExpressionToTemporary(builder, objectPath);
2873 }
2874 return {
2875 kind: 'branch',
2876 test: {...object},
2877 consequent: consequent.id,
2878 alternate,
2879 fallthrough: continuationBlock.id,
2880 id: makeInstructionId(0),
2881 loc,
2882 };
2883 });
2884 CompilerError.invariant(object !== null, {
2885 reason: 'Satisfy type checker',
2886 loc: GeneratedSource,
2887 });
2888
2889 /*
2890 * block to evaluate if the callee is non-null/undefined. arguments are lowered in this block to preserve
2891 * the semantic of conditional evaluation depending on the callee
2892 */
2893 builder.enterReserved(consequent, () => {
2894 const {value} = lowerMemberExpression(builder, expr, object);
2895 const temp = lowerValueToTemporary(builder, value);
2896 lowerValueToTemporary(builder, {
2897 kind: 'StoreLocal',
2898 lvalue: {kind: InstructionKind.Const, place: {...place}},
2899 value: {...temp},
2900 type: null,
2901 loc,
2902 });
2903 return {
2904 kind: 'goto',
2905 variant: GotoVariant.Break,
2906 block: continuationBlock.id,
2907 id: makeInstructionId(0),
2908 loc,
2909 };
2910 });
2911
2912 builder.terminateWithContinuation(
2913 {
2914 kind: 'optional',
2915 optional,
2916 test: testBlock,
2917 fallthrough: continuationBlock.id,
2918 id: makeInstructionId(0),
2919 loc,
2920 },
2921 continuationBlock,
2922 );
2923
2924 return {object, value: place};
2925 }
2926
2927 function lowerOptionalCallExpression(
2928 builder: HIRBuilder,
2929 expr: NodePath<t.OptionalCallExpression>,
2930 parentAlternate: BlockId | null,
2931 ): InstructionValue {
2932 const optional = expr.node.optional;
2933 const calleePath = expr.get('callee');
2934 const loc = expr.node.loc ?? GeneratedSource;
2935 const place = buildTemporaryPlace(builder, loc);
2936 const continuationBlock = builder.reserve(builder.currentBlockKind());
2937 const consequent = builder.reserve('value');
2938
2939 /*
2940 * block to evaluate if the callee is null/undefined, this sets the result of the call to undefined.
2941 * note that we only create an alternate when first entering an optional subtree of the ast: if this
2942 * is a child of an optional node, we use the alterate created by the parent.
2943 */
2944 const alternate =
2945 parentAlternate !== null
2946 ? parentAlternate
2947 : builder.enter('value', () => {
2948 const temp = lowerValueToTemporary(builder, {
2949 kind: 'Primitive',
2950 value: undefined,
2951 loc,
2952 });
2953 lowerValueToTemporary(builder, {
2954 kind: 'StoreLocal',
2955 lvalue: {kind: InstructionKind.Const, place: {...place}},
2956 value: {...temp},
2957 type: null,
2958 loc,
2959 });
2960 return {
2961 kind: 'goto',
2962 variant: GotoVariant.Break,
2963 block: continuationBlock.id,
2964 id: makeInstructionId(0),
2965 loc,
2966 };
2967 });
2968
2969 /*
2970 * Lower the callee within the test block to represent the fact that the code for the callee is
2971 * scoped within the optional
2972 */
2973 let callee:
2974 | {kind: 'CallExpression'; callee: Place}
2975 | {kind: 'MethodCall'; receiver: Place; property: Place};
2976 const testBlock = builder.enter('value', () => {
2977 if (calleePath.isOptionalCallExpression()) {
2978 // Recursively call lowerOptionalCallExpression to thread down the alternate block
2979 const value = lowerOptionalCallExpression(builder, calleePath, alternate);
2980 const valuePlace = lowerValueToTemporary(builder, value);
2981 callee = {
2982 kind: 'CallExpression',
2983 callee: valuePlace,
2984 };
2985 } else if (calleePath.isOptionalMemberExpression()) {
2986 const {object, value} = lowerOptionalMemberExpression(
2987 builder,
2988 calleePath,
2989 alternate,
2990 );
2991 callee = {
2992 kind: 'MethodCall',
2993 receiver: object,
2994 property: value,
2995 };
2996 } else if (calleePath.isMemberExpression()) {
2997 const memberExpr = lowerMemberExpression(builder, calleePath);
2998 const propertyPlace = lowerValueToTemporary(builder, memberExpr.value);
2999 callee = {
3000 kind: 'MethodCall',
3001 receiver: memberExpr.object,
3002 property: propertyPlace,
3003 };
3004 } else {
3005 callee = {
3006 kind: 'CallExpression',
3007 callee: lowerExpressionToTemporary(builder, calleePath),
3008 };
3009 }
3010 const testPlace =
3011 callee.kind === 'CallExpression' ? callee.callee : callee.property;
3012 return {
3013 kind: 'branch',
3014 test: {...testPlace},
3015 consequent: consequent.id,
3016 alternate,
3017 fallthrough: continuationBlock.id,
3018 id: makeInstructionId(0),
3019 loc,
3020 };
3021 });
3022
3023 /*
3024 * block to evaluate if the callee is non-null/undefined. arguments are lowered in this block to preserve
3025 * the semantic of conditional evaluation depending on the callee
3026 */
3027 builder.enterReserved(consequent, () => {
3028 const args = lowerArguments(builder, expr.get('arguments'));
3029 const temp = buildTemporaryPlace(builder, loc);
3030 if (callee.kind === 'CallExpression') {
3031 builder.push({
3032 id: makeInstructionId(0),
3033 lvalue: {...temp},
3034 value: {
3035 kind: 'CallExpression',
3036 callee: {...callee.callee},
3037 args,
3038 loc,
3039 },
3040 effects: null,
3041 loc,
3042 });
3043 } else {
3044 builder.push({
3045 id: makeInstructionId(0),
3046 lvalue: {...temp},
3047 value: {
3048 kind: 'MethodCall',
3049 receiver: {...callee.receiver},
3050 property: {...callee.property},
3051 args,
3052 loc,
3053 },
3054 effects: null,
3055 loc,
3056 });
3057 }
3058 lowerValueToTemporary(builder, {
3059 kind: 'StoreLocal',
3060 lvalue: {kind: InstructionKind.Const, place: {...place}},
3061 value: {...temp},
3062 type: null,
3063 loc,
3064 });
3065 return {
3066 kind: 'goto',
3067 variant: GotoVariant.Break,
3068 block: continuationBlock.id,
3069 id: makeInstructionId(0),
3070 loc,
3071 };
3072 });
3073
3074 builder.terminateWithContinuation(
3075 {
3076 kind: 'optional',
3077 optional,
3078 test: testBlock,
3079 fallthrough: continuationBlock.id,
3080 id: makeInstructionId(0),
3081 loc,
3082 },
3083 continuationBlock,
3084 );
3085
3086 return {kind: 'LoadLocal', place, loc: place.loc};
3087 }
3088
3089 /*
3090 * There are a few places where we do not preserve original evaluation ordering and/or control flow, such as
3091 * switch case test values and default values in destructuring (assignment patterns). In these cases we allow
3092 * simple expressions whose evaluation cannot be observed:
3093 * - primitives
3094 * - arrays/objects whose values are also safely reorderable.
3095 */
3096 function lowerReorderableExpression(
3097 builder: HIRBuilder,
3098 expr: NodePath<t.Expression>,
3099 ): Place {
3100 if (!isReorderableExpression(builder, expr, true)) {
3101 builder.recordError(
3102 new CompilerErrorDetail({
3103 reason: `(BuildHIR::node.lowerReorderableExpression) Expression type \`${expr.type}\` cannot be safely reordered`,
3104 category: ErrorCategory.Todo,
3105 loc: expr.node.loc ?? null,
3106 suggestions: null,
3107 }),
3108 );
3109 }
3110 return lowerExpressionToTemporary(builder, expr);
3111 }
3112
3113 function isReorderableExpression(
3114 builder: HIRBuilder,
3115 expr: NodePath<t.Expression>,
3116 allowLocalIdentifiers: boolean,
3117 ): boolean {
3118 switch (expr.node.type) {
3119 case 'Identifier': {
3120 const binding = builder.resolveIdentifier(expr as NodePath<t.Identifier>);
3121 if (binding.kind === 'Identifier') {
3122 return allowLocalIdentifiers;
3123 } else {
3124 // global, definitely safe
3125 return true;
3126 }
3127 }
3128 case 'TSInstantiationExpression': {
3129 const innerExpr = (expr as NodePath<t.TSInstantiationExpression>).get(
3130 'expression',
3131 ) as NodePath<t.Expression>;
3132 return isReorderableExpression(builder, innerExpr, allowLocalIdentifiers);
3133 }
3134 case 'RegExpLiteral':
3135 case 'StringLiteral':
3136 case 'NumericLiteral':
3137 case 'NullLiteral':
3138 case 'BooleanLiteral':
3139 case 'BigIntLiteral': {
3140 return true;
3141 }
3142 case 'UnaryExpression': {
3143 const unary = expr as NodePath<t.UnaryExpression>;
3144 switch (expr.node.operator) {
3145 case '!':
3146 case '+':
3147 case '-': {
3148 return isReorderableExpression(
3149 builder,
3150 unary.get('argument'),
3151 allowLocalIdentifiers,
3152 );
3153 }
3154 default: {
3155 return false;
3156 }
3157 }
3158 }
3159 case 'TSAsExpression':
3160 case 'TSNonNullExpression':
3161 case 'TypeCastExpression': {
3162 return isReorderableExpression(
3163 builder,
3164 (expr as NodePath<t.TypeCastExpression>).get('expression'),
3165 allowLocalIdentifiers,
3166 );
3167 }
3168 case 'LogicalExpression': {
3169 const logical = expr as NodePath<t.LogicalExpression>;
3170 return (
3171 isReorderableExpression(
3172 builder,
3173 logical.get('left'),
3174 allowLocalIdentifiers,
3175 ) &&
3176 isReorderableExpression(
3177 builder,
3178 logical.get('right'),
3179 allowLocalIdentifiers,
3180 )
3181 );
3182 }
3183 case 'ConditionalExpression': {
3184 const conditional = expr as NodePath<t.ConditionalExpression>;
3185 return (
3186 isReorderableExpression(
3187 builder,
3188 conditional.get('test'),
3189 allowLocalIdentifiers,
3190 ) &&
3191 isReorderableExpression(
3192 builder,
3193 conditional.get('consequent'),
3194 allowLocalIdentifiers,
3195 ) &&
3196 isReorderableExpression(
3197 builder,
3198 conditional.get('alternate'),
3199 allowLocalIdentifiers,
3200 )
3201 );
3202 }
3203 case 'ArrayExpression': {
3204 return (expr as NodePath<t.ArrayExpression>)
3205 .get('elements')
3206 .every(
3207 element =>
3208 element.isExpression() &&
3209 isReorderableExpression(builder, element, allowLocalIdentifiers),
3210 );
3211 }
3212 case 'ObjectExpression': {
3213 return (expr as NodePath<t.ObjectExpression>)
3214 .get('properties')
3215 .every(property => {
3216 if (!property.isObjectProperty() || property.node.computed) {
3217 return false;
3218 }
3219 const value = property.get('value');
3220 return (
3221 value.isExpression() &&
3222 isReorderableExpression(builder, value, allowLocalIdentifiers)
3223 );
3224 });
3225 }
3226 case 'MemberExpression': {
3227 /*
3228 * A common pattern is switch statements where the case test values are properties of a global,
3229 * eg `case ProductOptions.Option: { ... }`
3230 * We therefore allow expressions where the innermost object is a global identifier, and reject
3231 * all other member expressions (for now).
3232 */
3233 const test = expr as NodePath<t.MemberExpression>;
3234 let innerObject: NodePath<t.Expression> = test;
3235 while (innerObject.isMemberExpression()) {
3236 innerObject = innerObject.get('object');
3237 }
3238 if (
3239 innerObject.isIdentifier() &&
3240 builder.resolveIdentifier(innerObject).kind !== 'Identifier'
3241 ) {
3242 // This is a property/computed load from a global, that's safe to reorder
3243 return true;
3244 } else {
3245 return false;
3246 }
3247 }
3248 case 'ArrowFunctionExpression': {
3249 const fn = expr as NodePath<t.ArrowFunctionExpression>;
3250 const body = fn.get('body');
3251 if (body.node.type === 'BlockStatement') {
3252 return body.node.body.length === 0;
3253 } else {
3254 // For TypeScript
3255 invariant(body.isExpression(), 'Expected an expression');
3256 return isReorderableExpression(
3257 builder,
3258 body,
3259 /* disallow local identifiers in the body */ false,
3260 );
3261 }
3262 }
3263 case 'CallExpression': {
3264 const call = expr as NodePath<t.CallExpression>;
3265 const callee = call.get('callee');
3266 return (
3267 callee.isExpression() &&
3268 isReorderableExpression(builder, callee, allowLocalIdentifiers) &&
3269 call
3270 .get('arguments')
3271 .every(
3272 arg =>
3273 arg.isExpression() &&
3274 isReorderableExpression(builder, arg, allowLocalIdentifiers),
3275 )
3276 );
3277 }
3278 case 'NewExpression': {
3279 const newExpr = expr as NodePath<t.NewExpression>;
3280 const callee = newExpr.get('callee');
3281 return (
3282 callee.isExpression() &&
3283 isReorderableExpression(builder, callee, allowLocalIdentifiers) &&
3284 newExpr
3285 .get('arguments')
3286 .every(
3287 arg =>
3288 arg.isExpression() &&
3289 isReorderableExpression(builder, arg, allowLocalIdentifiers),
3290 )
3291 );
3292 }
3293 default: {
3294 return false;
3295 }
3296 }
3297 }
3298
3299 function lowerArguments(
3300 builder: HIRBuilder,
3301 expr: Array<
3302 NodePath<
3303 | t.Expression
3304 | t.SpreadElement
3305 | t.JSXNamespacedName
3306 | t.ArgumentPlaceholder
3307 >
3308 >,
3309 ): Array<Place | SpreadPattern> {
3310 let args: Array<Place | SpreadPattern> = [];
3311 for (const argPath of expr) {
3312 if (argPath.isSpreadElement()) {
3313 args.push({
3314 kind: 'Spread',
3315 place: lowerExpressionToTemporary(builder, argPath.get('argument')),
3316 });
3317 } else if (argPath.isExpression()) {
3318 args.push(lowerExpressionToTemporary(builder, argPath));
3319 } else {
3320 builder.recordError(
3321 new CompilerErrorDetail({
3322 reason: `(BuildHIR::lowerExpression) Handle ${argPath.type} arguments in CallExpression`,
3323 category: ErrorCategory.Todo,
3324 loc: argPath.node.loc ?? null,
3325 suggestions: null,
3326 }),
3327 );
3328 }
3329 }
3330 return args;
3331 }
3332
3333 type LoweredMemberExpression = {
3334 object: Place;
3335 property: Place | string | number;
3336 value: InstructionValue;
3337 };
3338 function lowerMemberExpression(
3339 builder: HIRBuilder,
3340 expr: NodePath<t.MemberExpression | t.OptionalMemberExpression>,
3341 loweredObject: Place | null = null,
3342 ): LoweredMemberExpression {
3343 const exprNode = expr.node;
3344 const exprLoc = exprNode.loc ?? GeneratedSource;
3345 const objectNode = expr.get('object');
3346 const propertyNode = expr.get('property');
3347 const object =
3348 loweredObject ?? lowerExpressionToTemporary(builder, objectNode);
3349
3350 if (!expr.node.computed || expr.node.property.type === 'NumericLiteral') {
3351 let property: PropertyLiteral;
3352 if (propertyNode.isIdentifier()) {
3353 property = makePropertyLiteral(propertyNode.node.name);
3354 } else if (propertyNode.isNumericLiteral()) {
3355 property = makePropertyLiteral(propertyNode.node.value);
3356 } else {
3357 builder.recordError(
3358 new CompilerErrorDetail({
3359 reason: `(BuildHIR::lowerMemberExpression) Handle ${propertyNode.type} property`,
3360 category: ErrorCategory.Todo,
3361 loc: propertyNode.node.loc ?? null,
3362 suggestions: null,
3363 }),
3364 );
3365 return {
3366 object,
3367 property: propertyNode.toString(),
3368 value: {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc},
3369 };
3370 }
3371 const value: InstructionValue = {
3372 kind: 'PropertyLoad',
3373 object: {...object},
3374 property,
3375 loc: exprLoc,
3376 };
3377 return {object, property, value};
3378 } else {
3379 if (!propertyNode.isExpression()) {
3380 builder.recordError(
3381 new CompilerErrorDetail({
3382 reason: `(BuildHIR::lowerMemberExpression) Expected Expression, got ${propertyNode.type} property`,
3383 category: ErrorCategory.Todo,
3384 loc: propertyNode.node.loc ?? null,
3385 suggestions: null,
3386 }),
3387 );
3388 return {
3389 object,
3390 property: propertyNode.toString(),
3391 value: {
3392 kind: 'UnsupportedNode',
3393 node: exprNode,
3394 loc: exprLoc,
3395 },
3396 };
3397 }
3398 const property = lowerExpressionToTemporary(builder, propertyNode);
3399 const value: InstructionValue = {
3400 kind: 'ComputedLoad',
3401 object: {...object},
3402 property: {...property},
3403 loc: exprLoc,
3404 };
3405 return {object, property, value};
3406 }
3407 }
3408
3409 function lowerJsxElementName(
3410 builder: HIRBuilder,
3411 exprPath: NodePath<
3412 t.JSXIdentifier | t.JSXMemberExpression | t.JSXNamespacedName
3413 >,
3414 ): Place | BuiltinTag {
3415 const exprNode = exprPath.node;
3416 const exprLoc = exprNode.loc ?? GeneratedSource;
3417 if (exprPath.isJSXIdentifier()) {
3418 const tag: string = exprPath.node.name;
3419 if (!tag.match(/^[a-z]/)) {
3420 const kind = getLoadKind(builder, exprPath);
3421 return lowerValueToTemporary(builder, {
3422 kind: kind,
3423 place: lowerIdentifier(builder, exprPath),
3424 loc: exprLoc,
3425 });
3426 } else {
3427 return {
3428 kind: 'BuiltinTag',
3429 name: tag,
3430 loc: exprLoc,
3431 };
3432 }
3433 } else if (exprPath.isJSXMemberExpression()) {
3434 return lowerJsxMemberExpression(builder, exprPath);
3435 } else if (exprPath.isJSXNamespacedName()) {
3436 const namespace = exprPath.node.namespace.name;
3437 const name = exprPath.node.name.name;
3438 const tag = `${namespace}:${name}`;
3439 if (namespace.indexOf(':') !== -1 || name.indexOf(':') !== -1) {
3440 builder.recordError(
3441 new CompilerErrorDetail({
3442 reason: `Expected JSXNamespacedName to have no colons in the namespace or name`,
3443 description: `Got \`${namespace}\` : \`${name}\``,
3444 category: ErrorCategory.Syntax,
3445 loc: exprPath.node.loc ?? null,
3446 suggestions: null,
3447 }),
3448 );
3449 }
3450 const place = lowerValueToTemporary(builder, {
3451 kind: 'Primitive',
3452 value: tag,
3453 loc: exprLoc,
3454 });
3455 return place;
3456 } else {
3457 builder.recordError(
3458 new CompilerErrorDetail({
3459 reason: `(BuildHIR::lowerJsxElementName) Handle ${exprPath.type} tags`,
3460 category: ErrorCategory.Todo,
3461 loc: exprPath.node.loc ?? null,
3462 suggestions: null,
3463 }),
3464 );
3465 return lowerValueToTemporary(builder, {
3466 kind: 'UnsupportedNode',
3467 node: exprNode,
3468 loc: exprLoc,
3469 });
3470 }
3471 }
3472
3473 function lowerJsxMemberExpression(
3474 builder: HIRBuilder,
3475 exprPath: NodePath<t.JSXMemberExpression>,
3476 ): Place {
3477 const loc = exprPath.node.loc ?? GeneratedSource;
3478 const object = exprPath.get('object');
3479 let objectPlace: Place;
3480 if (object.isJSXMemberExpression()) {
3481 objectPlace = lowerJsxMemberExpression(builder, object);
3482 } else {
3483 CompilerError.invariant(object.isJSXIdentifier(), {
3484 reason: `TypeScript refinement fail: expected 'JsxIdentifier', got \`${object.node.type}\``,
3485 loc: object.node.loc ?? GeneratedSource,
3486 });
3487
3488 const kind = getLoadKind(builder, object);
3489 objectPlace = lowerValueToTemporary(builder, {
3490 kind: kind,
3491 place: lowerIdentifier(builder, object),
3492 loc: exprPath.node.loc ?? GeneratedSource,
3493 });
3494 }
3495 const property = exprPath.get('property').node.name;
3496 return lowerValueToTemporary(builder, {
3497 kind: 'PropertyLoad',
3498 object: objectPlace,
3499 property: makePropertyLiteral(property),
3500 loc,
3501 });
3502 }
3503
3504 function lowerJsxElement(
3505 builder: HIRBuilder,
3506 exprPath: NodePath<
3507 | t.JSXText
3508 | t.JSXExpressionContainer
3509 | t.JSXSpreadChild
3510 | t.JSXElement
3511 | t.JSXFragment
3512 >,
3513 ): Place | null {
3514 const exprNode = exprPath.node;
3515 const exprLoc = exprNode.loc ?? GeneratedSource;
3516 if (exprPath.isJSXElement() || exprPath.isJSXFragment()) {
3517 return lowerExpressionToTemporary(builder, exprPath);
3518 } else if (exprPath.isJSXExpressionContainer()) {
3519 const expression = exprPath.get('expression');
3520 if (expression.isJSXEmptyExpression()) {
3521 return null;
3522 } else {
3523 CompilerError.invariant(expression.isExpression(), {
3524 reason: `(BuildHIR::lowerJsxElement) Expected Expression but found ${expression.type}!`,
3525 loc: expression.node.loc ?? GeneratedSource,
3526 });
3527 return lowerExpressionToTemporary(builder, expression);
3528 }
3529 } else if (exprPath.isJSXText()) {
3530 let text: string | null;
3531 if (builder.fbtDepth > 0) {
3532 /*
3533 * FBT whitespace normalization differs from standard JSX.
3534 * https://github.com/facebook/fbt/blob/0b4e0d13c30bffd0daa2a75715d606e3587b4e40/packages/babel-plugin-fbt/src/FbtUtil.js#L76-L87
3535 * Since the fbt transform runs after, let's just preserve all
3536 * whitespace in FBT subtrees as is.
3537 */
3538 text = exprPath.node.value;
3539 } else {
3540 text = trimJsxText(exprPath.node.value);
3541 }
3542
3543 if (text === null) {
3544 return null;
3545 }
3546 const place = lowerValueToTemporary(builder, {
3547 kind: 'JSXText',
3548 value: text,
3549 loc: exprLoc,
3550 });
3551 return place;
3552 } else {
3553 builder.recordError(
3554 new CompilerErrorDetail({
3555 reason: `(BuildHIR::lowerJsxElement) Unhandled JsxElement, got: ${exprPath.type}`,
3556 category: ErrorCategory.Todo,
3557 loc: exprPath.node.loc ?? null,
3558 suggestions: null,
3559 }),
3560 );
3561 const place = lowerValueToTemporary(builder, {
3562 kind: 'UnsupportedNode',
3563 node: exprNode,
3564 loc: exprLoc,
3565 });
3566 return place;
3567 }
3568 }
3569
3570 /*
3571 * Trims whitespace according to the JSX spec:
3572 * > JSX removes whitespace at the beginning and ending of a line.
3573 * > It also removes blank lines. New lines adjacent to tags are removed;
3574 * > new lines that occur in the middle of string literals are condensed
3575 * > into a single space.
3576 *
3577 * From https://legacy.reactjs.org/docs/jsx-in-depth.html#string-literals-1
3578 *
3579 * Implementation adapted from Babel:
3580 * https://github.com/babel/babel/blob/54d30f206057be64b496d2da1ec8c49d244ba4e4/packages/babel-types/src/utils/react/cleanJSXElementLiteralChild.ts#L5
3581 */
3582 function trimJsxText(original: string): string | null {
3583 const lines = original.split(/\r\n|\n|\r/);
3584
3585 let lastNonEmptyLine = 0;
3586
3587 for (let i = 0; i < lines.length; i++) {
3588 if (lines[i].match(/[^ \t]/)) {
3589 lastNonEmptyLine = i;
3590 }
3591 }
3592
3593 let str = '';
3594
3595 for (let i = 0; i < lines.length; i++) {
3596 const line = lines[i];
3597
3598 const isFirstLine = i === 0;
3599 const isLastLine = i === lines.length - 1;
3600 const isLastNonEmptyLine = i === lastNonEmptyLine;
3601
3602 // replace rendered whitespace tabs with spaces
3603 let trimmedLine = line.replace(/\t/g, ' ');
3604
3605 // trim whitespace touching a newline
3606 if (!isFirstLine) {
3607 trimmedLine = trimmedLine.replace(/^[ ]+/, '');
3608 }
3609
3610 // trim whitespace touching an endline
3611 if (!isLastLine) {
3612 trimmedLine = trimmedLine.replace(/[ ]+$/, '');
3613 }
3614
3615 if (trimmedLine) {
3616 if (!isLastNonEmptyLine) {
3617 trimmedLine += ' ';
3618 }
3619
3620 str += trimmedLine;
3621 }
3622 }
3623
3624 if (str.length !== 0) {
3625 return str;
3626 } else {
3627 return null;
3628 }
3629 }
3630
3631 function lowerFunctionToValue(
3632 builder: HIRBuilder,
3633 expr: NodePath<
3634 t.FunctionExpression | t.ArrowFunctionExpression | t.FunctionDeclaration
3635 >,
3636 ): InstructionValue {
3637 const exprNode = expr.node;
3638 const exprLoc = exprNode.loc ?? GeneratedSource;
3639 const loweredFunc = lowerFunction(builder, expr);
3640 return {
3641 kind: 'FunctionExpression',
3642 name: loweredFunc.func.id,
3643 nameHint: null,
3644 type: expr.node.type,
3645 loc: exprLoc,
3646 loweredFunc,
3647 };
3648 }
3649
3650 function lowerFunction(
3651 builder: HIRBuilder,
3652 expr: NodePath<
3653 | t.FunctionExpression
3654 | t.ArrowFunctionExpression
3655 | t.FunctionDeclaration
3656 | t.ObjectMethod
3657 >,
3658 ): LoweredFunction {
3659 const componentScope: Scope = builder.environment.parentFunction.scope;
3660 const capturedContext = gatherCapturedContext(expr, componentScope);
3661
3662 /*
3663 * TODO(gsn): In the future, we could only pass in the context identifiers
3664 * that are actually used by this function and it's nested functions, rather
3665 * than all context identifiers.
3666 *
3667 * This isn't a problem in practice because use Babel's scope analysis to
3668 * identify the correct references.
3669 */
3670 const loweredFunc = lower(
3671 expr,
3672 builder.environment,
3673 builder.bindings,
3674 new Map([...builder.context, ...capturedContext]),
3675 );
3676 return {
3677 func: loweredFunc,
3678 };
3679 }
3680
3681 function lowerExpressionToTemporary(
3682 builder: HIRBuilder,
3683 exprPath: NodePath<t.Expression>,
3684 ): Place {
3685 const value = lowerExpression(builder, exprPath);
3686 return lowerValueToTemporary(builder, value);
3687 }
3688
3689 export function lowerValueToTemporary(
3690 builder: HIRBuilder,
3691 value: InstructionValue,
3692 ): Place {
3693 if (value.kind === 'LoadLocal' && value.place.identifier.name === null) {
3694 return value.place;
3695 }
3696 const place: Place = buildTemporaryPlace(builder, value.loc);
3697 builder.push({
3698 id: makeInstructionId(0),
3699 lvalue: {...place},
3700 value: value,
3701 effects: null,
3702 loc: value.loc,
3703 });
3704 return place;
3705 }
3706
3707 function lowerIdentifier(
3708 builder: HIRBuilder,
3709 exprPath: NodePath<t.Identifier | t.JSXIdentifier>,
3710 ): Place {
3711 const exprNode = exprPath.node;
3712 const exprLoc = exprNode.loc ?? GeneratedSource;
3713 const binding = builder.resolveIdentifier(exprPath);
3714 switch (binding.kind) {
3715 case 'Identifier': {
3716 const place: Place = {
3717 kind: 'Identifier',
3718 identifier: binding.identifier,
3719 effect: Effect.Unknown,
3720 reactive: false,
3721 loc: exprLoc,
3722 };
3723 return place;
3724 }
3725 default: {
3726 if (binding.kind === 'Global' && binding.name === 'eval') {
3727 builder.recordError(
3728 new CompilerErrorDetail({
3729 reason: `The 'eval' function is not supported`,
3730 description:
3731 'Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler',
3732 category: ErrorCategory.UnsupportedSyntax,
3733 loc: exprPath.node.loc ?? null,
3734 suggestions: null,
3735 }),
3736 );
3737 }
3738 return lowerValueToTemporary(builder, {
3739 kind: 'LoadGlobal',
3740 binding,
3741 loc: exprLoc,
3742 });
3743 }
3744 }
3745 }
3746
3747 // Creates a temporary Identifier and Place referencing that identifier.
3748 function buildTemporaryPlace(builder: HIRBuilder, loc: SourceLocation): Place {
3749 const place: Place = {
3750 kind: 'Identifier',
3751 identifier: builder.makeTemporary(loc),
3752 effect: Effect.Unknown,
3753 reactive: false,
3754 loc,
3755 };
3756 return place;
3757 }
3758
3759 function getStoreKind(
3760 builder: HIRBuilder,
3761 identifier: NodePath<t.Identifier>,
3762 ): 'StoreLocal' | 'StoreContext' {
3763 const isContext = builder.isContextIdentifier(identifier);
3764 return isContext ? 'StoreContext' : 'StoreLocal';
3765 }
3766
3767 function getLoadKind(
3768 builder: HIRBuilder,
3769 identifier: NodePath<t.Identifier | t.JSXIdentifier>,
3770 ): 'LoadLocal' | 'LoadContext' {
3771 const isContext = builder.isContextIdentifier(identifier);
3772 return isContext ? 'LoadContext' : 'LoadLocal';
3773 }
3774
3775 function lowerIdentifierForAssignment(
3776 builder: HIRBuilder,
3777 loc: SourceLocation,
3778 kind: InstructionKind,
3779 path: NodePath<t.Identifier>,
3780 ): Place | {kind: 'Global'; name: string} | null {
3781 const binding = builder.resolveIdentifier(path);
3782 if (binding.kind !== 'Identifier') {
3783 if (kind === InstructionKind.Reassign) {
3784 return {kind: 'Global', name: path.node.name};
3785 } else {
3786 // Else its an internal error bc we couldn't find the binding
3787 builder.recordError(
3788 new CompilerErrorDetail({
3789 reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
3790 category: ErrorCategory.Invariant,
3791 loc: path.node.loc ?? null,
3792 suggestions: null,
3793 }),
3794 );
3795 return null;
3796 }
3797 } else if (
3798 binding.bindingKind === 'const' &&
3799 kind === InstructionKind.Reassign
3800 ) {
3801 builder.recordError(
3802 new CompilerErrorDetail({
3803 reason: `Cannot reassign a \`const\` variable`,
3804 category: ErrorCategory.Syntax,
3805 loc: path.node.loc ?? null,
3806 description:
3807 binding.identifier.name != null
3808 ? `\`${binding.identifier.name.value}\` is declared as const`
3809 : null,
3810 }),
3811 );
3812 return null;
3813 }
3814
3815 const place: Place = {
3816 kind: 'Identifier',
3817 identifier: binding.identifier,
3818 effect: Effect.Unknown,
3819 reactive: false,
3820 loc,
3821 };
3822 return place;
3823 }
3824
3825 function lowerAssignment(
3826 builder: HIRBuilder,
3827 loc: SourceLocation,
3828 kind: InstructionKind,
3829 lvaluePath: NodePath<t.LVal>,
3830 value: Place,
3831 assignmentKind: 'Destructure' | 'Assignment',
3832 ): InstructionValue {
3833 const lvalueNode = lvaluePath.node;
3834 switch (lvalueNode.type) {
3835 case 'Identifier': {
3836 const lvalue = lvaluePath as NodePath<t.Identifier>;
3837 const place = lowerIdentifierForAssignment(builder, loc, kind, lvalue);
3838 if (place === null) {
3839 return {
3840 kind: 'UnsupportedNode',
3841 loc: lvalue.node.loc ?? GeneratedSource,
3842 node: lvalue.node,
3843 };
3844 } else if (place.kind === 'Global') {
3845 const temporary = lowerValueToTemporary(builder, {
3846 kind: 'StoreGlobal',
3847 name: place.name,
3848 value,
3849 loc,
3850 });
3851 return {kind: 'LoadLocal', place: temporary, loc: temporary.loc};
3852 }
3853 const isHoistedIdentifier = builder.environment.isHoistedIdentifier(
3854 lvalue.node,
3855 );
3856
3857 let temporary;
3858 if (builder.isContextIdentifier(lvalue)) {
3859 if (kind === InstructionKind.Const && !isHoistedIdentifier) {
3860 builder.recordError(
3861 new CompilerErrorDetail({
3862 reason: `Expected \`const\` declaration not to be reassigned`,
3863 category: ErrorCategory.Syntax,
3864 loc: lvalue.node.loc ?? null,
3865 suggestions: null,
3866 }),
3867 );
3868 }
3869
3870 if (
3871 kind !== InstructionKind.Const &&
3872 kind !== InstructionKind.Reassign &&
3873 kind !== InstructionKind.Let &&
3874 kind !== InstructionKind.Function
3875 ) {
3876 builder.recordError(
3877 new CompilerErrorDetail({
3878 reason: `Unexpected context variable kind`,
3879 category: ErrorCategory.Syntax,
3880 loc: lvalue.node.loc ?? null,
3881 suggestions: null,
3882 }),
3883 );
3884 temporary = lowerValueToTemporary(builder, {
3885 kind: 'UnsupportedNode',
3886 node: lvalueNode,
3887 loc: lvalueNode.loc ?? GeneratedSource,
3888 });
3889 } else {
3890 temporary = lowerValueToTemporary(builder, {
3891 kind: 'StoreContext',
3892 lvalue: {place: {...place}, kind},
3893 value,
3894 loc,
3895 });
3896 }
3897 } else {
3898 const typeAnnotation = lvalue.get('typeAnnotation');
3899 let type: t.FlowType | t.TSType | null;
3900 if (typeAnnotation.isTSTypeAnnotation()) {
3901 const typePath = typeAnnotation.get('typeAnnotation');
3902 type = typePath.node;
3903 } else if (typeAnnotation.isTypeAnnotation()) {
3904 const typePath = typeAnnotation.get('typeAnnotation');
3905 type = typePath.node;
3906 } else {
3907 type = null;
3908 }
3909 temporary = lowerValueToTemporary(builder, {
3910 kind: 'StoreLocal',
3911 lvalue: {place: {...place}, kind},
3912 value,
3913 type,
3914 loc,
3915 });
3916 }
3917 return {kind: 'LoadLocal', place: temporary, loc: temporary.loc};
3918 }
3919 case 'MemberExpression': {
3920 // This can only occur because of a coding error, parsers enforce this condition
3921 CompilerError.invariant(kind === InstructionKind.Reassign, {
3922 reason: 'MemberExpression may only appear in an assignment expression',
3923 loc: lvaluePath.node.loc ?? GeneratedSource,
3924 });
3925 const lvalue = lvaluePath as NodePath<t.MemberExpression>;
3926 const property = lvalue.get('property');
3927 const object = lowerExpressionToTemporary(builder, lvalue.get('object'));
3928 if (!lvalue.node.computed || lvalue.get('property').isNumericLiteral()) {
3929 let temporary;
3930 if (property.isIdentifier()) {
3931 temporary = lowerValueToTemporary(builder, {
3932 kind: 'PropertyStore',
3933 object,
3934 property: makePropertyLiteral(property.node.name),
3935 value,
3936 loc,
3937 });
3938 } else if (property.isNumericLiteral()) {
3939 temporary = lowerValueToTemporary(builder, {
3940 kind: 'PropertyStore',
3941 object,
3942 property: makePropertyLiteral(property.node.value),
3943 value,
3944 loc,
3945 });
3946 } else {
3947 builder.recordError(
3948 new CompilerErrorDetail({
3949 reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in MemberExpression`,
3950 category: ErrorCategory.Todo,
3951 loc: property.node.loc ?? null,
3952 suggestions: null,
3953 }),
3954 );
3955 return {kind: 'UnsupportedNode', node: lvalueNode, loc};
3956 }
3957 return {kind: 'LoadLocal', place: temporary, loc: temporary.loc};
3958 } else {
3959 if (!property.isExpression()) {
3960 builder.recordError(
3961 new CompilerErrorDetail({
3962 reason:
3963 '(BuildHIR::lowerAssignment) Expected private name to appear as a non-computed property',
3964 category: ErrorCategory.Todo,
3965 loc: property.node.loc ?? null,
3966 suggestions: null,
3967 }),
3968 );
3969 return {kind: 'UnsupportedNode', node: lvalueNode, loc};
3970 }
3971 const propertyPlace = lowerExpressionToTemporary(builder, property);
3972 const temporary = lowerValueToTemporary(builder, {
3973 kind: 'ComputedStore',
3974 object,
3975 property: propertyPlace,
3976 value,
3977 loc,
3978 });
3979 return {kind: 'LoadLocal', place: temporary, loc: temporary.loc};
3980 }
3981 }
3982 case 'ArrayPattern': {
3983 const lvalue = lvaluePath as NodePath<t.ArrayPattern>;
3984 const elements = lvalue.get('elements');
3985 const items: ArrayPattern['items'] = [];
3986 const followups: Array<{place: Place; path: NodePath<t.LVal>}> = [];
3987 /*
3988 * A given destructuring statement must contain all declarations or all
3989 * reassignments. This is enforced by the parser, but we rewrite nested
3990 * destructuring into assignment to a temporary. Therefore, if we see
3991 * any reassignments that are nested destructuring we fall back to
3992 * using temporaries for all variables, and emitting the actual reassignments
3993 * in follow-up statements
3994 */
3995 const forceTemporaries =
3996 kind === InstructionKind.Reassign &&
3997 (elements.some(element => !element.isIdentifier()) ||
3998 elements.some(
3999 element =>
4000 element.isIdentifier() &&
4001 (getStoreKind(builder, element) !== 'StoreLocal' ||
4002 builder.resolveIdentifier(element).kind !== 'Identifier'),
4003 ));
4004 for (let i = 0; i < elements.length; i++) {
4005 const element = elements[i];
4006 if (element.node == null) {
4007 items.push({
4008 kind: 'Hole',
4009 });
4010 continue;
4011 }
4012 if (element.isRestElement()) {
4013 const argument = element.get('argument');
4014 if (
4015 argument.isIdentifier() &&
4016 !forceTemporaries &&
4017 (assignmentKind === 'Assignment' ||
4018 getStoreKind(builder, argument) === 'StoreLocal')
4019 ) {
4020 const identifier = lowerIdentifierForAssignment(
4021 builder,
4022 element.node.loc ?? GeneratedSource,
4023 kind,
4024 argument,
4025 );
4026 if (identifier === null) {
4027 continue;
4028 } else if (identifier.kind === 'Global') {
4029 builder.recordError(
4030 new CompilerErrorDetail({
4031 category: ErrorCategory.Todo,
4032 reason:
4033 'Expected reassignment of globals to enable forceTemporaries',
4034 loc: element.node.loc ?? GeneratedSource,
4035 }),
4036 );
4037 continue;
4038 }
4039 items.push({
4040 kind: 'Spread',
4041 place: identifier,
4042 });
4043 } else {
4044 const temp = buildTemporaryPlace(
4045 builder,
4046 element.node.loc ?? GeneratedSource,
4047 );
4048 promoteTemporary(temp.identifier);
4049 items.push({
4050 kind: 'Spread',
4051 place: {...temp},
4052 });
4053 followups.push({place: temp, path: argument as NodePath<t.LVal>}); // TODO remove type cast
4054 }
4055 } else if (
4056 element.isIdentifier() &&
4057 !forceTemporaries &&
4058 (assignmentKind === 'Assignment' ||
4059 getStoreKind(builder, element) === 'StoreLocal')
4060 ) {
4061 const identifier = lowerIdentifierForAssignment(
4062 builder,
4063 element.node.loc ?? GeneratedSource,
4064 kind,
4065 element,
4066 );
4067 if (identifier === null) {
4068 continue;
4069 } else if (identifier.kind === 'Global') {
4070 builder.recordError(
4071 new CompilerErrorDetail({
4072 category: ErrorCategory.Todo,
4073 reason:
4074 'Expected reassignment of globals to enable forceTemporaries',
4075 loc: element.node.loc ?? GeneratedSource,
4076 }),
4077 );
4078 continue;
4079 }
4080 items.push(identifier);
4081 } else {
4082 const temp = buildTemporaryPlace(
4083 builder,
4084 element.node.loc ?? GeneratedSource,
4085 );
4086 promoteTemporary(temp.identifier);
4087 items.push({...temp});
4088 followups.push({place: temp, path: element as NodePath<t.LVal>}); // TODO remove type cast
4089 }
4090 }
4091 const temporary = lowerValueToTemporary(builder, {
4092 kind: 'Destructure',
4093 lvalue: {
4094 kind,
4095 pattern: {
4096 kind: 'ArrayPattern',
4097 items,
4098 loc: lvalue.node.loc ?? GeneratedSource,
4099 },
4100 },
4101 value,
4102 loc,
4103 });
4104 for (const {place, path} of followups) {
4105 lowerAssignment(
4106 builder,
4107 path.node.loc ?? loc,
4108 kind,
4109 path,
4110 place,
4111 assignmentKind,
4112 );
4113 }
4114 return {kind: 'LoadLocal', place: temporary, loc: value.loc};
4115 }
4116 case 'ObjectPattern': {
4117 const lvalue = lvaluePath as NodePath<t.ObjectPattern>;
4118 const propertiesPaths = lvalue.get('properties');
4119 const properties: ObjectPattern['properties'] = [];
4120 const followups: Array<{place: Place; path: NodePath<t.LVal>}> = [];
4121 /*
4122 * A given destructuring statement must contain all declarations or all
4123 * reassignments. This is enforced by the parser, but we rewrite nested
4124 * destructuring into assignment to a temporary. Therefore, if we see
4125 * any reassignments that are nested destructuring we fall back to
4126 * using temporaries for all variables, and emitting the actual reassignments
4127 * in follow-up statements
4128 */
4129 const forceTemporaries =
4130 kind === InstructionKind.Reassign &&
4131 propertiesPaths.some(
4132 property =>
4133 property.isRestElement() ||
4134 (property.isObjectProperty() &&
4135 (!property.get('value').isIdentifier() ||
4136 builder.resolveIdentifier(
4137 property.get('value') as NodePath<t.Identifier>,
4138 ).kind !== 'Identifier')),
4139 );
4140 for (let i = 0; i < propertiesPaths.length; i++) {
4141 const property = propertiesPaths[i];
4142 if (property.isRestElement()) {
4143 const argument = property.get('argument');
4144 if (!argument.isIdentifier()) {
4145 builder.recordError(
4146 new CompilerErrorDetail({
4147 reason: `(BuildHIR::lowerAssignment) Handle ${argument.node.type} rest element in ObjectPattern`,
4148 category: ErrorCategory.Todo,
4149 loc: argument.node.loc ?? null,
4150 suggestions: null,
4151 }),
4152 );
4153 continue;
4154 }
4155 if (
4156 forceTemporaries ||
4157 getStoreKind(builder, argument) === 'StoreContext'
4158 ) {
4159 const temp = buildTemporaryPlace(
4160 builder,
4161 property.node.loc ?? GeneratedSource,
4162 );
4163 promoteTemporary(temp.identifier);
4164 properties.push({
4165 kind: 'Spread',
4166 place: {...temp},
4167 });
4168 followups.push({place: temp, path: argument as NodePath<t.LVal>}); // TODO remove type cast
4169 } else {
4170 const identifier = lowerIdentifierForAssignment(
4171 builder,
4172 property.node.loc ?? GeneratedSource,
4173 kind,
4174 argument,
4175 );
4176 if (identifier === null) {
4177 continue;
4178 } else if (identifier.kind === 'Global') {
4179 builder.recordError(
4180 new CompilerErrorDetail({
4181 category: ErrorCategory.Todo,
4182 reason:
4183 'Expected reassignment of globals to enable forceTemporaries',
4184 loc: property.node.loc ?? GeneratedSource,
4185 }),
4186 );
4187 continue;
4188 }
4189 properties.push({
4190 kind: 'Spread',
4191 place: identifier,
4192 });
4193 }
4194 } else {
4195 // TODO: this should always be true given the if/else
4196 if (!property.isObjectProperty()) {
4197 builder.recordError(
4198 new CompilerErrorDetail({
4199 reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in ObjectPattern`,
4200 category: ErrorCategory.Todo,
4201 loc: property.node.loc ?? null,
4202 suggestions: null,
4203 }),
4204 );
4205 continue;
4206 }
4207 if (property.node.computed) {
4208 builder.recordError(
4209 new CompilerErrorDetail({
4210 reason: `(BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern`,
4211 category: ErrorCategory.Todo,
4212 loc: property.node.loc ?? null,
4213 suggestions: null,
4214 }),
4215 );
4216 continue;
4217 }
4218 const loweredKey = lowerObjectPropertyKey(builder, property);
4219 if (!loweredKey) {
4220 continue;
4221 }
4222 const element = property.get('value');
4223 if (!element.isLVal()) {
4224 builder.recordError(
4225 new CompilerErrorDetail({
4226 reason: `(BuildHIR::lowerAssignment) Expected object property value to be an LVal, got: ${element.type}`,
4227 category: ErrorCategory.Todo,
4228 loc: element.node.loc ?? null,
4229 suggestions: null,
4230 }),
4231 );
4232 continue;
4233 }
4234 if (
4235 element.isIdentifier() &&
4236 !forceTemporaries &&
4237 (assignmentKind === 'Assignment' ||
4238 getStoreKind(builder, element) === 'StoreLocal')
4239 ) {
4240 const identifier = lowerIdentifierForAssignment(
4241 builder,
4242 element.node.loc ?? GeneratedSource,
4243 kind,
4244 element,
4245 );
4246 if (identifier === null) {
4247 continue;
4248 } else if (identifier.kind === 'Global') {
4249 builder.recordError(
4250 new CompilerErrorDetail({
4251 category: ErrorCategory.Todo,
4252 reason:
4253 'Expected reassignment of globals to enable forceTemporaries',
4254 loc: element.node.loc ?? GeneratedSource,
4255 }),
4256 );
4257 continue;
4258 }
4259 properties.push({
4260 kind: 'ObjectProperty',
4261 type: 'property',
4262 place: identifier,
4263 key: loweredKey,
4264 });
4265 } else {
4266 const temp = buildTemporaryPlace(
4267 builder,
4268 element.node.loc ?? GeneratedSource,
4269 );
4270 promoteTemporary(temp.identifier);
4271 properties.push({
4272 kind: 'ObjectProperty',
4273 type: 'property',
4274 place: {...temp},
4275 key: loweredKey,
4276 });
4277 followups.push({place: temp, path: element as NodePath<t.LVal>}); // TODO remove type cast
4278 }
4279 }
4280 }
4281 const temporary = lowerValueToTemporary(builder, {
4282 kind: 'Destructure',
4283 lvalue: {
4284 kind,
4285 pattern: {
4286 kind: 'ObjectPattern',
4287 properties,
4288 loc: lvalue.node.loc ?? GeneratedSource,
4289 },
4290 },
4291 value,
4292 loc,
4293 });
4294 for (const {place, path} of followups) {
4295 lowerAssignment(
4296 builder,
4297 path.node.loc ?? loc,
4298 kind,
4299 path,
4300 place,
4301 assignmentKind,
4302 );
4303 }
4304 return {kind: 'LoadLocal', place: temporary, loc: value.loc};
4305 }
4306 case 'AssignmentPattern': {
4307 const lvalue = lvaluePath as NodePath<t.AssignmentPattern>;
4308 const loc = lvalue.node.loc ?? GeneratedSource;
4309 const temp = buildTemporaryPlace(builder, loc);
4310
4311 const testBlock = builder.reserve('value');
4312 const continuationBlock = builder.reserve(builder.currentBlockKind());
4313
4314 const consequent = builder.enter('value', () => {
4315 /*
4316 * Because we reorder evaluation, we restrict the allowed default values to those where
4317 * evaluation order is unobservable
4318 */
4319 const defaultValue = lowerReorderableExpression(
4320 builder,
4321 lvalue.get('right'),
4322 );
4323 lowerValueToTemporary(builder, {
4324 kind: 'StoreLocal',
4325 lvalue: {kind: InstructionKind.Const, place: {...temp}},
4326 value: {...defaultValue},
4327 type: null,
4328 loc,
4329 });
4330 return {
4331 kind: 'goto',
4332 variant: GotoVariant.Break,
4333 block: continuationBlock.id,
4334 id: makeInstructionId(0),
4335 loc,
4336 };
4337 });
4338
4339 const alternate = builder.enter('value', () => {
4340 lowerValueToTemporary(builder, {
4341 kind: 'StoreLocal',
4342 lvalue: {kind: InstructionKind.Const, place: {...temp}},
4343 value: {...value},
4344 type: null,
4345 loc,
4346 });
4347 return {
4348 kind: 'goto',
4349 variant: GotoVariant.Break,
4350 block: continuationBlock.id,
4351 id: makeInstructionId(0),
4352 loc,
4353 };
4354 });
4355 builder.terminateWithContinuation(
4356 {
4357 kind: 'ternary',
4358 test: testBlock.id,
4359 fallthrough: continuationBlock.id,
4360 id: makeInstructionId(0),
4361 loc,
4362 },
4363 testBlock,
4364 );
4365 const undef = lowerValueToTemporary(builder, {
4366 kind: 'Primitive',
4367 value: undefined,
4368 loc,
4369 });
4370 const test = lowerValueToTemporary(builder, {
4371 kind: 'BinaryExpression',
4372 left: {...value},
4373 operator: '===',
4374 right: {...undef},
4375 loc,
4376 });
4377 builder.terminateWithContinuation(
4378 {
4379 kind: 'branch',
4380 test: {...test},
4381 consequent,
4382 alternate,
4383 fallthrough: continuationBlock.id,
4384 id: makeInstructionId(0),
4385 loc,
4386 },
4387 continuationBlock,
4388 );
4389
4390 return lowerAssignment(
4391 builder,
4392 loc,
4393 kind,
4394 lvalue.get('left'),
4395 temp,
4396 assignmentKind,
4397 );
4398 }
4399 default: {
4400 builder.recordError(
4401 new CompilerErrorDetail({
4402 reason: `(BuildHIR::lowerAssignment) Handle ${lvaluePath.type} assignments`,
4403 category: ErrorCategory.Todo,
4404 loc: lvaluePath.node.loc ?? null,
4405 suggestions: null,
4406 }),
4407 );
4408 return {kind: 'UnsupportedNode', node: lvalueNode, loc};
4409 }
4410 }
4411 }
4412
4413 function captureScopes({from, to}: {from: Scope; to: Scope}): Set<Scope> {
4414 let scopes: Set<Scope> = new Set();
4415 while (from) {
4416 scopes.add(from);
4417
4418 if (from === to) {
4419 break;
4420 }
4421
4422 from = from.parent;
4423 }
4424 return scopes;
4425 }
4426
4427 /**
4428 * Returns a mapping of "context" identifiers — references to free variables that
4429 * will become part of the function expression's `context` array — along with the
4430 * source location of their first reference within the function.
4431 */
4432 function gatherCapturedContext(
4433 fn: NodePath<
4434 | t.FunctionExpression
4435 | t.ArrowFunctionExpression
4436 | t.FunctionDeclaration
4437 | t.ObjectMethod
4438 >,
4439 componentScope: Scope,
4440 ): Map<t.Identifier, SourceLocation> {
4441 const capturedIds = new Map<t.Identifier, SourceLocation>();
4442
4443 /*
4444 * Capture all the scopes from the parent of this function up to and including
4445 * the component scope.
4446 */
4447 const pureScopes: Set<Scope> = captureScopes({
4448 from: fn.scope.parent,
4449 to: componentScope,
4450 });
4451
4452 function handleMaybeDependency(
4453 path: NodePath<t.Identifier> | NodePath<t.JSXOpeningElement>,
4454 ): void {
4455 // Base context variable to depend on
4456 let baseIdentifier: NodePath<t.Identifier> | NodePath<t.JSXIdentifier>;
4457 if (path.isJSXOpeningElement()) {
4458 const name = path.get('name');
4459 if (!(name.isJSXMemberExpression() || name.isJSXIdentifier())) {
4460 // TODO: should JSX namespaced names be handled here as well?
4461 return;
4462 }
4463 let current: NodePath<t.JSXMemberExpression | t.JSXIdentifier> = name;
4464 while (current.isJSXMemberExpression()) {
4465 current = current.get('object');
4466 }
4467 invariant(
4468 current.isJSXIdentifier(),
4469 'Invalid logic in gatherCapturedDeps',
4470 );
4471 baseIdentifier = current;
4472 } else {
4473 baseIdentifier = path;
4474 }
4475
4476 /*
4477 * Skip dependency path, as we already tried to recursively add it (+ all subexpressions)
4478 * as a dependency.
4479 */
4480 path.skip();
4481
4482 // Add the base identifier binding as a dependency.
4483 const binding = baseIdentifier.scope.getBinding(baseIdentifier.node.name);
4484 if (
4485 binding !== undefined &&
4486 pureScopes.has(binding.scope) &&
4487 !capturedIds.has(binding.identifier)
4488 ) {
4489 capturedIds.set(
4490 binding.identifier,
4491 path.node.loc ?? binding.identifier.loc ?? GeneratedSource,
4492 );
4493 }
4494 }
4495
4496 fn.traverse({
4497 TypeAnnotation(path) {
4498 path.skip();
4499 },
4500 TSTypeAnnotation(path) {
4501 path.skip();
4502 },
4503 TypeAlias(path) {
4504 path.skip();
4505 },
4506 TSTypeAliasDeclaration(path) {
4507 path.skip();
4508 },
4509 Expression(path) {
4510 if (path.isAssignmentExpression()) {
4511 /*
4512 * Babel has a bug where it doesn't visit the LHS of an
4513 * AssignmentExpression if it's an Identifier. Work around it by explicitly
4514 * visiting it.
4515 */
4516 const left = path.get('left');
4517 if (left.isIdentifier()) {
4518 handleMaybeDependency(left);
4519 }
4520 return;
4521 } else if (path.isJSXElement()) {
4522 handleMaybeDependency(path.get('openingElement'));
4523 } else if (path.isIdentifier()) {
4524 handleMaybeDependency(path);
4525 }
4526 },
4527 });
4528
4529 return capturedIds;
4530 }
4531
4532 function notNull<T>(value: T | null): value is T {
4533 return value !== null;
4534 }
4535
4536 export function lowerType(node: t.FlowType | t.TSType): Type {
4537 switch (node.type) {
4538 case 'GenericTypeAnnotation': {
4539 const id = node.id;
4540 if (id.type === 'Identifier' && id.name === 'Array') {
4541 return {kind: 'Object', shapeId: BuiltInArrayId};
4542 }
4543 return makeType();
4544 }
4545 case 'TSTypeReference': {
4546 const typeName = node.typeName;
4547 if (typeName.type === 'Identifier' && typeName.name === 'Array') {
4548 return {kind: 'Object', shapeId: BuiltInArrayId};
4549 }
4550 return makeType();
4551 }
4552 case 'ArrayTypeAnnotation':
4553 case 'TSArrayType': {
4554 return {kind: 'Object', shapeId: BuiltInArrayId};
4555 }
4556 case 'BooleanLiteralTypeAnnotation':
4557 case 'BooleanTypeAnnotation':
4558 case 'NullLiteralTypeAnnotation':
4559 case 'NumberLiteralTypeAnnotation':
4560 case 'NumberTypeAnnotation':
4561 case 'StringLiteralTypeAnnotation':
4562 case 'StringTypeAnnotation':
4563 case 'TSBooleanKeyword':
4564 case 'TSNullKeyword':
4565 case 'TSNumberKeyword':
4566 case 'TSStringKeyword':
4567 case 'TSSymbolKeyword':
4568 case 'TSUndefinedKeyword':
4569 case 'TSVoidKeyword':
4570 case 'VoidTypeAnnotation': {
4571 return {kind: 'Primitive'};
4572 }
4573 default: {
4574 return makeType();
4575 }
4576 }
4577 }