[compiler] Support optional/logical/etc within try/catch (#35606)
Adds support for value terminals (optional/logical/ternary/sequence) within try/catch clauses. Try/catch expressions insert maybe-throw terminals after each instruction, but BuildReactiveFunction's value block extraction was not expecting these terminals. The fix is to roughly treat maybe-throw similarly to goto, falling through to the continuation block, but there are a few edge cases to handle. I've also added extensive tests, including testing that errors correctly flow to the catch handler.
Joseph Savona committed
Feb 2, 2026 at 09:27 UTC
b8a6bfa22c8cfc11863f2373fde44ae36695cd0b
30 files changed
+1313
-341
compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts
+4
@@ -583,6 +583,10 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
583
testBlock = fn.body.blocks.get(terminal.fallthrough)!;
584
break;
585
}
586
+ case 'maybe-throw': {
587
+ testBlock = fn.body.blocks.get(terminal.continuation)!;
588
+ break;
589
+ }
590
default: {
591
CompilerError.invariant(false, {
592
reason: `Unexpected terminal in optional`,
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts
+223
-178
@@ -22,6 +22,7 @@ import {
22
ReactiveBreakTerminal,
23
ReactiveContinueTerminal,
24
ReactiveFunction,
25
+ ReactiveInstruction,
26
ReactiveLogicalValue,
27
ReactiveSequenceValue,
28
ReactiveTerminalStatement,
@@ -62,6 +63,84 @@ class Driver {
63
this.cx = cx;
64
}
65
66
+ /*
67
+ * Wraps a continuation result with preceding instructions. If there are no
68
+ * instructions, returns the continuation as-is. Otherwise, wraps the continuation's
69
+ * value in a SequenceExpression with the instructions prepended.
70
+ */
71
+ wrapWithSequence(
72
+ instructions: Array<ReactiveInstruction>,
73
+ continuation: {
74
+ block: BlockId;
75
+ value: ReactiveValue;
76
+ place: Place;
77
+ id: InstructionId;
78
+ },
79
+ loc: SourceLocation,
80
+ ): {block: BlockId; value: ReactiveValue; place: Place; id: InstructionId} {
81
+ if (instructions.length === 0) {
82
+ return continuation;
83
+ }
84
+ const sequence: ReactiveSequenceValue = {
85
+ kind: 'SequenceExpression',
86
+ instructions,
87
+ id: continuation.id,
88
+ value: continuation.value,
89
+ loc,
90
+ };
91
+ return {
92
+ block: continuation.block,
93
+ value: sequence,
94
+ place: continuation.place,
95
+ id: continuation.id,
96
+ };
97
+ }
98
+
99
+ /*
100
+ * Extracts the result value from instructions at the end of a value block.
101
+ * Value blocks generally end in a StoreLocal to assign the value of the
102
+ * expression. These StoreLocal instructions can be pruned since we represent
103
+ * value blocks as compound values in ReactiveFunction (no phis). However,
104
+ * it's also possible to have a value block that ends in an AssignmentExpression,
105
+ * which we need to keep. So we only prune StoreLocal for temporaries.
106
+ */
107
+ extractValueBlockResult(
108
+ instructions: BasicBlock['instructions'],
109
+ blockId: BlockId,
110
+ loc: SourceLocation,
111
+ ): {block: BlockId; place: Place; value: ReactiveValue; id: InstructionId} {
112
+ CompilerError.invariant(instructions.length !== 0, {
113
+ reason: `Expected non-empty instructions in extractValueBlockResult`,
114
+ description: null,
115
+ loc,
116
+ });
117
+ const instr = instructions.at(-1)!;
118
+ let place: Place = instr.lvalue;
119
+ let value: ReactiveValue = instr.value;
120
+ if (
121
+ value.kind === 'StoreLocal' &&
122
+ value.lvalue.place.identifier.name === null
123
+ ) {
124
+ place = value.lvalue.place;
125
+ value = {
126
+ kind: 'LoadLocal',
127
+ place: value.value,
128
+ loc: value.value.loc,
129
+ };
130
+ }
131
+ if (instructions.length === 1) {
132
+ return {block: blockId, place, value, id: instr.id};
133
+ }
134
+ const sequence: ReactiveSequenceValue = {
135
+ kind: 'SequenceExpression',
136
+ instructions: instructions.slice(0, -1),
137
+ id: instr.id,
138
+ value,
139
+ loc,
140
+ };
141
+ return {block: blockId, place, value: sequence, id: instr.id};
142
+ }
143
+
144
traverseBlock(block: BasicBlock): ReactiveBlock {
145
const blockValue: ReactiveBlock = [];
146
this.visitBlock(block, blockValue);
@@ -846,164 +925,138 @@ class Driver {
925
}
926
927
visitValueBlock(
849
- id: BlockId,
928
+ blockId: BlockId,
929
loc: SourceLocation,
930
+ fallthrough: BlockId | null = null,
931
): {block: BlockId; value: ReactiveValue; place: Place; id: InstructionId} {
852
- const defaultBlock = this.cx.ir.blocks.get(id)!;
853
- if (defaultBlock.terminal.kind === 'branch') {
854
- const instructions = defaultBlock.instructions;
855
- if (instructions.length === 0) {
932
+ const block = this.cx.ir.blocks.get(blockId)!;
933
+ // If we've reached the fallthrough block, stop recursing
934
+ if (fallthrough !== null && blockId === fallthrough) {
935
+ CompilerError.invariant(false, {
936
+ reason: 'Did not expect to reach the fallthrough of a value block',
937
+ description: `Reached bb${blockId}, which is the fallthrough for this value block`,
938
+ loc,
939
+ });
940
+ }
941
+ if (block.terminal.kind === 'branch') {
942
+ if (block.instructions.length === 0) {
943
return {
857
- block: defaultBlock.id,
858
- place: defaultBlock.terminal.test,
944
+ block: block.id,
945
+ place: block.terminal.test,
946
value: {
947
kind: 'LoadLocal',
861
- place: defaultBlock.terminal.test,
862
- loc: defaultBlock.terminal.test.loc,
948
+ place: block.terminal.test,
949
+ loc: block.terminal.test.loc,
950
},
864
- id: defaultBlock.terminal.id,
865
- };
866
- } else if (defaultBlock.instructions.length === 1) {
867
- const instr = defaultBlock.instructions[0]!;
868
- CompilerError.invariant(
869
- instr.lvalue.identifier.id ===
870
- defaultBlock.terminal.test.identifier.id,
871
- {
872
- reason:
873
- 'Expected branch block to end in an instruction that sets the test value',
874
- loc: instr.lvalue.loc,
875
- },
876
- );
877
- return {
878
- block: defaultBlock.id,
879
- place: instr.lvalue!,
880
- value: instr.value,
881
- id: instr.id,
882
- };
883
- } else {
884
- const instr = defaultBlock.instructions.at(-1)!;
885
- const sequence: ReactiveSequenceValue = {
886
- kind: 'SequenceExpression',
887
- instructions: defaultBlock.instructions.slice(0, -1),
888
- id: instr.id,
889
- value: instr.value,
890
- loc: loc,
891
- };
892
- return {
893
- block: defaultBlock.id,
894
- place: defaultBlock.terminal.test,
895
- value: sequence,
896
- id: defaultBlock.terminal.id,
951
+ id: block.terminal.id,
952
};
953
}
899
- } else if (defaultBlock.terminal.kind === 'goto') {
900
- const instructions = defaultBlock.instructions;
901
- if (instructions.length === 0) {
954
+ return this.extractValueBlockResult(block.instructions, block.id, loc);
955
+ } else if (block.terminal.kind === 'goto') {
956
+ if (block.instructions.length === 0) {
957
CompilerError.invariant(false, {
903
- reason: 'Expected goto value block to have at least one instruction',
904
- loc: GeneratedSource,
958
+ reason: 'Unexpected empty block with `goto` terminal',
959
+ description: `Block bb${block.id} is empty`,
960
+ loc,
961
});
906
- } else if (defaultBlock.instructions.length === 1) {
907
- const instr = defaultBlock.instructions[0]!;
908
- let place: Place = instr.lvalue;
909
- let value: ReactiveValue = instr.value;
910
- if (
911
- /*
912
- * Value blocks generally end in a StoreLocal to assign the value of the
913
- * expression for this branch. These StoreLocal instructions can be pruned,
914
- * since we represent the value blocks as a compund value in ReactiveFunction
915
- * (no phis). However, it's also possible to have a value block that ends in
916
- * an AssignmentExpression, which we need to keep. So we only prune
917
- * StoreLocal for temporaries — any named/promoted values must be used
918
- * elsewhere and aren't safe to prune.
919
- */
920
- value.kind === 'StoreLocal' &&
921
- value.lvalue.place.identifier.name === null
922
- ) {
923
- place = value.lvalue.place;
924
- value = {
925
- kind: 'LoadLocal',
926
- place: value.value,
927
- loc: value.value.loc,
928
- };
929
- }
930
- return {
931
- block: defaultBlock.id,
932
- place,
933
- value,
934
- id: instr.id,
935
- };
936
- } else {
937
- const instr = defaultBlock.instructions.at(-1)!;
938
- let place: Place = instr.lvalue;
939
- let value: ReactiveValue = instr.value;
940
- if (
941
- /*
942
- * Value blocks generally end in a StoreLocal to assign the value of the
943
- * expression for this branch. These StoreLocal instructions can be pruned,
944
- * since we represent the value blocks as a compund value in ReactiveFunction
945
- * (no phis). However, it's also possible to have a value block that ends in
946
- * an AssignmentExpression, which we need to keep. So we only prune
947
- * StoreLocal for temporaries — any named/promoted values must be used
948
- * elsewhere and aren't safe to prune.
949
- */
950
- value.kind === 'StoreLocal' &&
951
- value.lvalue.place.identifier.name === null
952
- ) {
953
- place = value.lvalue.place;
954
- value = {
955
- kind: 'LoadLocal',
956
- place: value.value,
957
- loc: value.value.loc,
958
- };
959
- }
960
- const sequence: ReactiveSequenceValue = {
961
- kind: 'SequenceExpression',
962
- instructions: defaultBlock.instructions.slice(0, -1),
963
- id: instr.id,
964
- value,
965
- loc: loc,
966
- };
967
- return {
968
- block: defaultBlock.id,
969
- place,
970
- value: sequence,
971
- id: instr.id,
972
- };
962
}
963
+ return this.extractValueBlockResult(block.instructions, block.id, loc);
964
+ } else if (block.terminal.kind === 'maybe-throw') {
965
+ /*
966
+ * ReactiveFunction does not explicitly model maybe-throw semantics,
967
+ * so maybe-throw terminals in value blocks flatten away. In general
968
+ * we recurse to the continuation block.
969
+ *
970
+ * However, if the last portion
971
+ * of the value block is a potentially throwing expression, then the
972
+ * value block could be of the form
973
+ * ```
974
+ * bb1:
975
+ * ...StoreLocal for the value block...
976
+ * maybe-throw continuation=bb2
977
+ * bb2:
978
+ * goto (exit the value block)
979
+ * ```
980
+ *
981
+ * Ie what would have been a StoreLocal+goto is split up because of
982
+ * the maybe-throw. We detect this case and return the value of the
983
+ * current block as the result of the value block
984
+ */
985
+ const continuationId = block.terminal.continuation;
986
+ const continuationBlock = this.cx.ir.blocks.get(continuationId)!;
987
+ if (
988
+ continuationBlock.instructions.length === 0 &&
989
+ continuationBlock.terminal.kind === 'goto'
990
+ ) {
991
+ return this.extractValueBlockResult(
992
+ block.instructions,
993
+ continuationBlock.id,
994
+ loc,
995
+ );
996
+ }
997
+
998
+ const continuation = this.visitValueBlock(
999
+ continuationId,
1000
+ loc,
1001
+ fallthrough,
1002
+ );
1003
+ return this.wrapWithSequence(block.instructions, continuation, loc);
1004
} else {
1005
/*
1006
* The value block ended in a value terminal, recurse to get the value
977
- * of that terminal
1007
+ * of that terminal and stitch them together in a sequence.
1008
*/
979
- const init = this.visitValueBlockTerminal(defaultBlock.terminal);
980
- // Code following the logical terminal
1009
+ const init = this.visitValueBlockTerminal(block.terminal);
1010
const final = this.visitValueBlock(init.fallthrough, loc);
982
- // Stitch the two together...
983
- const sequence: ReactiveSequenceValue = {
984
- kind: 'SequenceExpression',
985
- instructions: [
986
- ...defaultBlock.instructions,
987
- {
988
- id: init.id,
989
- loc,
990
- lvalue: init.place,
991
- value: init.value,
992
- },
1011
+ return this.wrapWithSequence(
1012
+ [
1013
+ ...block.instructions,
1014
+ {id: init.id, loc, lvalue: init.place, value: init.value},
1015
],
994
- id: final.id,
995
- value: final.value,
1016
+ final,
1017
loc,
997
- };
998
- return {
999
- block: init.fallthrough,
1000
- value: sequence,
1001
- place: final.place,
1002
- id: final.id,
1003
- };
1018
+ );
1019
}
1020
}
1021
1022
+ /*
1023
+ * Visits the test block of a value terminal (optional, logical, ternary) and
1024
+ * returns the result along with the branch terminal. Throws a todo error if
1025
+ * the test block does not end in a branch terminal.
1026
+ */
1027
+ visitTestBlock(
1028
+ testBlockId: BlockId,
1029
+ loc: SourceLocation,
1030
+ terminalKind: string,
1031
+ ): {
1032
+ test: {
1033
+ block: BlockId;
1034
+ value: ReactiveValue;
1035
+ place: Place;
1036
+ id: InstructionId;
1037
+ };
1038
+ branch: {consequent: BlockId; alternate: BlockId; loc: SourceLocation};
1039
+ } {
1040
+ const test = this.visitValueBlock(testBlockId, loc);
1041
+ const testBlock = this.cx.ir.blocks.get(test.block)!;
1042
+ if (testBlock.terminal.kind !== 'branch') {
1043
+ CompilerError.throwTodo({
1044
+ reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for ${terminalKind} test block`,
1045
+ description: null,
1046
+ loc: testBlock.terminal.loc,
1047
+ suggestions: null,
1048
+ });
1049
+ }
1050
+ return {
1051
+ test,
1052
+ branch: {
1053
+ consequent: testBlock.terminal.consequent,
1054
+ alternate: testBlock.terminal.alternate,
1055
+ loc: testBlock.terminal.loc,
1056
+ },
1057
+ };
1058
+ }
1059
+
1060
visitValueBlockTerminal(terminal: Terminal): {
1061
value: ReactiveValue;
1062
place: Place;
@@ -1012,7 +1065,11 @@ class Driver {
1065
} {
1066
switch (terminal.kind) {
1067
case 'sequence': {
1015
- const block = this.visitValueBlock(terminal.block, terminal.loc);
1068
+ const block = this.visitValueBlock(
1069
+ terminal.block,
1070
+ terminal.loc,
1071
+ terminal.fallthrough,
1072
+ );
1073
return {
1074
value: block.value,
1075
place: block.place,
@@ -1021,26 +1078,22 @@ class Driver {
1078
};
1079
}
1080
case 'optional': {
1024
- const test = this.visitValueBlock(terminal.test, terminal.loc);
1025
- const testBlock = this.cx.ir.blocks.get(test.block)!;
1026
- if (testBlock.terminal.kind !== 'branch') {
1027
- CompilerError.throwTodo({
1028
- reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for optional test block`,
1029
- description: null,
1030
- loc: testBlock.terminal.loc,
1031
- suggestions: null,
1032
- });
1033
- }
1081
+ const {test, branch} = this.visitTestBlock(
1082
+ terminal.test,
1083
+ terminal.loc,
1084
+ 'optional',
1085
+ );
1086
const consequent = this.visitValueBlock(
1035
- testBlock.terminal.consequent,
1087
+ branch.consequent,
1088
terminal.loc,
1089
+ terminal.fallthrough,
1090
);
1091
const call: ReactiveSequenceValue = {
1092
kind: 'SequenceExpression',
1093
instructions: [
1094
{
1095
id: test.id,
1043
- loc: testBlock.terminal.loc,
1096
+ loc: branch.loc,
1097
lvalue: test.place,
1098
value: test.value,
1099
},
@@ -1063,20 +1116,15 @@ class Driver {
1116
};
1117
}
1118
case 'logical': {
1066
- const test = this.visitValueBlock(terminal.test, terminal.loc);
1067
- const testBlock = this.cx.ir.blocks.get(test.block)!;
1068
- if (testBlock.terminal.kind !== 'branch') {
1069
- CompilerError.throwTodo({
1070
- reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for logical test block`,
1071
- description: null,
1072
- loc: testBlock.terminal.loc,
1073
- suggestions: null,
1074
- });
1075
- }
1076
-
1119
+ const {test, branch} = this.visitTestBlock(
1120
+ terminal.test,
1121
+ terminal.loc,
1122
+ 'logical',
1123
+ );
1124
const leftFinal = this.visitValueBlock(
1078
- testBlock.terminal.consequent,
1125
+ branch.consequent,
1126
terminal.loc,
1127
+ terminal.fallthrough,
1128
);
1129
const left: ReactiveSequenceValue = {
1130
kind: 'SequenceExpression',
@@ -1093,8 +1141,9 @@ class Driver {
1141
loc: terminal.loc,
1142
};
1143
const right = this.visitValueBlock(
1096
- testBlock.terminal.alternate,
1144
+ branch.alternate,
1145
terminal.loc,
1146
+ terminal.fallthrough,
1147
);
1148
const value: ReactiveLogicalValue = {
1149
kind: 'LogicalExpression',
@@ -1111,23 +1160,20 @@ class Driver {
1160
};
1161
}
1162
case 'ternary': {
1114
- const test = this.visitValueBlock(terminal.test, terminal.loc);
1115
- const testBlock = this.cx.ir.blocks.get(test.block)!;
1116
- if (testBlock.terminal.kind !== 'branch') {
1117
- CompilerError.throwTodo({
1118
- reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for ternary test block`,
1119
- description: null,
1120
- loc: testBlock.terminal.loc,
1121
- suggestions: null,
1122
- });
1123
- }
1163
+ const {test, branch} = this.visitTestBlock(
1164
+ terminal.test,
1165
+ terminal.loc,
1166
+ 'ternary',
1167
+ );
1168
const consequent = this.visitValueBlock(
1125
- testBlock.terminal.consequent,
1169
+ branch.consequent,
1170
terminal.loc,
1171
+ terminal.fallthrough,
1172
);
1173
const alternate = this.visitValueBlock(
1129
- testBlock.terminal.alternate,
1174
+ branch.alternate,
1175
terminal.loc,
1176
+ terminal.fallthrough,
1177
);
1178
const value: ReactiveTernaryValue = {
1179
kind: 'ConditionalExpression',
@@ -1145,11 +1191,10 @@ class Driver {
1191
};
1192
}
1193
case 'maybe-throw': {
1148
- CompilerError.throwTodo({
1149
- reason: `Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement`,
1194
+ CompilerError.invariant(false, {
1195
+ reason: `Unexpected maybe-throw in visitValueBlockTerminal - should be handled in visitValueBlock`,
1196
description: null,
1197
loc: terminal.loc,
1152
- suggestions: null,
1198
});
1199
}
1200
case 'label': {
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts
+4
@@ -1016,6 +1016,10 @@ export function findOptionalPlaces(
1016
testBlock = fn.body.blocks.get(terminal.block)!;
1017
break;
1018
}
1019
+ case 'maybe-throw': {
1020
+ testBlock = fn.body.blocks.get(terminal.continuation)!;
1021
+ break;
1022
+ }
1023
default: {
1024
CompilerError.invariant(false, {
1025
reason: `Unexpected terminal in optional`,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.expect.md
deleted
-34
@@ -1,34 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-const Foo = ({json}) => {
6
- try {
7
- const foo = JSON.parse(json)?.foo;
8
- return <span>{foo}</span>;
9
- } catch {
10
- return null;
11
- }
12
-};
13
-
14
-```
15
-
16
-
17
-## Error
18
-
19
-```
20
-Found 1 error:
21
-
22
-Invariant: Unexpected terminal in optional
23
-
24
-error.bug-invariant-unexpected-terminal-in-optional.ts:3:16
25
- 1 | const Foo = ({json}) => {
26
- 2 | try {
27
-> 3 | const foo = JSON.parse(json)?.foo;
28
- | ^^^^ Unexpected maybe-throw in optional
29
- 4 | return <span>{foo}</span>;
30
- 5 | } catch {
31
- 6 | return null;
32
-```
33
-
34
-
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.js
deleted
-8
@@ -1,8 +0,0 @@
1
-const Foo = ({json}) => {
2
- try {
3
- const foo = JSON.parse(json)?.foo;
4
- return <span>{foo}</span>;
5
- } catch {
6
- return null;
7
- }
8
-};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-logical-expression-within-try-catch.expect.md
deleted
-35
@@ -1,35 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-function Component(props) {
6
- let result;
7
- try {
8
- result = props.cond && props.foo;
9
- } catch (e) {
10
- console.log(e);
11
- }
12
- return result;
13
-}
14
-
15
-```
16
-
17
-
18
-## Error
19
-
20
-```
21
-Found 1 error:
22
-
23
-Todo: Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement
24
-
25
-error.todo-logical-expression-within-try-catch.ts:4:13
26
- 2 | let result;
27
- 3 | try {
28
-> 4 | result = props.cond && props.foo;
29
- | ^^^^^ Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement
30
- 5 | } catch (e) {
31
- 6 | console.log(e);
32
- 7 | }
33
-```
34
-
35
-
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-logical-expression-within-try-catch.js
deleted
-9
@@ -1,9 +0,0 @@
1
-function Component(props) {
2
- let result;
3
- try {
4
- result = props.cond && props.foo;
5
- } catch (e) {
6
- console.log(e);
7
- }
8
- return result;
9
-}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-logical-expr.expect.md
deleted
-37
@@ -1,37 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-import {useNoAlias} from 'shared-runtime';
6
-
7
-function useFoo(props: {value: {x: string; y: string} | null}) {
8
- const value = props.value;
9
- return useNoAlias(value?.x, value?.y) ?? {};
10
-}
11
-
12
-export const FIXTURE_ENTRYPONT = {
13
- fn: useFoo,
14
- props: [{value: null}],
15
-};
16
-
17
-```
18
-
19
-
20
-## Error
21
-
22
-```
23
-Found 1 error:
24
-
25
-Todo: Unexpected terminal kind `optional` for logical test block
26
-
27
-error.todo-optional-call-chain-in-logical-expr.ts:5:30
28
- 3 | function useFoo(props: {value: {x: string; y: string} | null}) {
29
- 4 | const value = props.value;
30
-> 5 | return useNoAlias(value?.x, value?.y) ?? {};
31
- | ^^^^^^^^ Unexpected terminal kind `optional` for logical test block
32
- 6 | }
33
- 7 |
34
- 8 | export const FIXTURE_ENTRYPONT = {
35
-```
36
-
37
-
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-ternary.expect.md
deleted
-37
@@ -1,37 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-import {useNoAlias} from 'shared-runtime';
6
-
7
-function useFoo(props: {value: {x: string; y: string} | null}) {
8
- const value = props.value;
9
- return useNoAlias(value?.x, value?.y) ? {} : null;
10
-}
11
-
12
-export const FIXTURE_ENTRYPONT = {
13
- fn: useFoo,
14
- props: [{value: null}],
15
-};
16
-
17
-```
18
-
19
-
20
-## Error
21
-
22
-```
23
-Found 1 error:
24
-
25
-Todo: Unexpected terminal kind `optional` for ternary test block
26
-
27
-error.todo-optional-call-chain-in-ternary.ts:5:30
28
- 3 | function useFoo(props: {value: {x: string; y: string} | null}) {
29
- 4 | const value = props.value;
30
-> 5 | return useNoAlias(value?.x, value?.y) ? {} : null;
31
- | ^^^^^^^^ Unexpected terminal kind `optional` for ternary test block
32
- 6 | }
33
- 7 |
34
- 8 | export const FIXTURE_ENTRYPONT = {
35
-```
36
-
37
-
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-declaration-for-all-identifiers.expect.md
+5
-3
@@ -18,13 +18,15 @@ function Foo() {
18
```
19
Found 1 error:
20
21
-Todo: Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement
21
+Invariant: Expected a variable declaration
22
23
-error.todo-repro-declaration-for-all-identifiers.ts:5:20
23
+Got ExpressionStatement.
24
+
25
+error.todo-repro-declaration-for-all-identifiers.ts:5:4
26
3 | // NOTE: this fixture previously failed during LeaveSSA;
27
4 | // double-check this code when supporting value blocks in try/catch
28
> 5 | for (let i = 0; i < 2; i++) {}
27
- | ^ Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement
29
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Expected a variable declaration
30
6 | } catch {}
31
7 | }
32
8 |
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-logical-expr.expect.md
new
+37
@@ -0,0 +1,37 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import {useNoAlias} from 'shared-runtime';
6
+
7
+function useFoo(props: {value: {x: string; y: string} | null}) {
8
+ const value = props.value;
9
+ return useNoAlias(value?.x, value?.y) ?? {};
10
+}
11
+
12
+export const FIXTURE_ENTRYPONT = {
13
+ fn: useFoo,
14
+ props: [{value: null}],
15
+};
16
+
17
+```
18
+
19
+## Code
20
+
21
+```javascript
22
+import { useNoAlias } from "shared-runtime";
23
+
24
+function useFoo(props) {
25
+ const value = props.value;
26
+ return useNoAlias(value?.x, value?.y) ?? {};
27
+}
28
+
29
+export const FIXTURE_ENTRYPONT = {
30
+ fn: useFoo,
31
+ props: [{ value: null }],
32
+};
33
+
34
+```
35
+
36
+### Eval output
37
+(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-logical-expr.ts
renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-ternary.expect.md
new
+37
@@ -0,0 +1,37 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import {useNoAlias} from 'shared-runtime';
6
+
7
+function useFoo(props: {value: {x: string; y: string} | null}) {
8
+ const value = props.value;
9
+ return useNoAlias(value?.x, value?.y) ? {} : null;
10
+}
11
+
12
+export const FIXTURE_ENTRYPONT = {
13
+ fn: useFoo,
14
+ props: [{value: null}],
15
+};
16
+
17
+```
18
+
19
+## Code
20
+
21
+```javascript
22
+import { useNoAlias } from "shared-runtime";
23
+
24
+function useFoo(props) {
25
+ const value = props.value;
26
+ return useNoAlias(value?.x, value?.y) ? {} : null;
27
+}
28
+
29
+export const FIXTURE_ENTRYPONT = {
30
+ fn: useFoo,
31
+ props: [{ value: null }],
32
+};
33
+
34
+```
35
+
36
+### Eval output
37
+(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-ternary.ts
renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-and-optional.expect.md
new
+85
@@ -0,0 +1,85 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component({cond, obj, items}) {
6
+ try {
7
+ // items.length is accessed WITHIN the && expression
8
+ const result = cond && obj?.value && items.length;
9
+ return <div>{String(result)}</div>;
10
+ } catch {
11
+ return <div>error</div>;
12
+ }
13
+}
14
+
15
+export const FIXTURE_ENTRYPOINT = {
16
+ fn: Component,
17
+ params: [{cond: true, obj: {value: 'hello'}, items: [1, 2]}],
18
+ sequentialRenders: [
19
+ {cond: true, obj: {value: 'hello'}, items: [1, 2]},
20
+ {cond: true, obj: {value: 'hello'}, items: [1, 2]},
21
+ {cond: true, obj: {value: 'world'}, items: [1, 2, 3]},
22
+ {cond: false, obj: {value: 'hello'}, items: [1]},
23
+ {cond: true, obj: null, items: [1]},
24
+ {cond: true, obj: {value: 'test'}, items: null}, // errors because items.length throws WITHIN the && chain
25
+ {cond: null, obj: {value: 'test'}, items: [1]},
26
+ ],
27
+};
28
+
29
+```
30
+
31
+## Code
32
+
33
+```javascript
34
+import { c as _c } from "react/compiler-runtime";
35
+function Component(t0) {
36
+ const $ = _c(3);
37
+ const { cond, obj, items } = t0;
38
+ try {
39
+ const result = cond && obj?.value && items.length;
40
+ const t1 = String(result);
41
+ let t2;
42
+ if ($[0] !== t1) {
43
+ t2 = <div>{t1}</div>;
44
+ $[0] = t1;
45
+ $[1] = t2;
46
+ } else {
47
+ t2 = $[1];
48
+ }
49
+ return t2;
50
+ } catch {
51
+ let t1;
52
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
53
+ t1 = <div>error</div>;
54
+ $[2] = t1;
55
+ } else {
56
+ t1 = $[2];
57
+ }
58
+ return t1;
59
+ }
60
+}
61
+
62
+export const FIXTURE_ENTRYPOINT = {
63
+ fn: Component,
64
+ params: [{ cond: true, obj: { value: "hello" }, items: [1, 2] }],
65
+ sequentialRenders: [
66
+ { cond: true, obj: { value: "hello" }, items: [1, 2] },
67
+ { cond: true, obj: { value: "hello" }, items: [1, 2] },
68
+ { cond: true, obj: { value: "world" }, items: [1, 2, 3] },
69
+ { cond: false, obj: { value: "hello" }, items: [1] },
70
+ { cond: true, obj: null, items: [1] },
71
+ { cond: true, obj: { value: "test" }, items: null }, // errors because items.length throws WITHIN the && chain
72
+ { cond: null, obj: { value: "test" }, items: [1] },
73
+ ],
74
+};
75
+
76
+```
77
+
78
+### Eval output
79
+(kind: ok) <div>2</div>
80
+<div>2</div>
81
+<div>3</div>
82
+<div>false</div>
83
+<div>undefined</div>
84
+<div>error</div>
85
+<div>null</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-and-optional.js
new
+23
@@ -0,0 +1,23 @@
1
+function Component({cond, obj, items}) {
2
+ try {
3
+ // items.length is accessed WITHIN the && expression
4
+ const result = cond && obj?.value && items.length;
5
+ return <div>{String(result)}</div>;
6
+ } catch {
7
+ return <div>error</div>;
8
+ }
9
+}
10
+
11
+export const FIXTURE_ENTRYPOINT = {
12
+ fn: Component,
13
+ params: [{cond: true, obj: {value: 'hello'}, items: [1, 2]}],
14
+ sequentialRenders: [
15
+ {cond: true, obj: {value: 'hello'}, items: [1, 2]},
16
+ {cond: true, obj: {value: 'hello'}, items: [1, 2]},
17
+ {cond: true, obj: {value: 'world'}, items: [1, 2, 3]},
18
+ {cond: false, obj: {value: 'hello'}, items: [1]},
19
+ {cond: true, obj: null, items: [1]},
20
+ {cond: true, obj: {value: 'test'}, items: null}, // errors because items.length throws WITHIN the && chain
21
+ {cond: null, obj: {value: 'test'}, items: [1]},
22
+ ],
23
+};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-expression.expect.md
new
+69
@@ -0,0 +1,69 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component(props) {
6
+ let result;
7
+ try {
8
+ // items.length is accessed WITHIN the && expression
9
+ result = props.cond && props.foo && props.items.length;
10
+ } catch (e) {
11
+ result = 'error';
12
+ }
13
+ return result;
14
+}
15
+
16
+export const FIXTURE_ENTRYPOINT = {
17
+ fn: Component,
18
+ params: [{cond: true, foo: true, items: [1, 2, 3]}],
19
+ sequentialRenders: [
20
+ {cond: true, foo: true, items: [1, 2, 3]},
21
+ {cond: true, foo: true, items: [1, 2, 3]},
22
+ {cond: true, foo: true, items: [1, 2, 3, 4]},
23
+ {cond: false, foo: true, items: [1, 2, 3]},
24
+ {cond: true, foo: false, items: [1, 2, 3]},
25
+ {cond: true, foo: true, items: null}, // errors because props.items.length throws
26
+ {cond: null, foo: true, items: [1]},
27
+ ],
28
+};
29
+
30
+```
31
+
32
+## Code
33
+
34
+```javascript
35
+function Component(props) {
36
+ let result;
37
+ try {
38
+ result = props.cond && props.foo && props.items.length;
39
+ } catch (t0) {
40
+ result = "error";
41
+ }
42
+
43
+ return result;
44
+}
45
+
46
+export const FIXTURE_ENTRYPOINT = {
47
+ fn: Component,
48
+ params: [{ cond: true, foo: true, items: [1, 2, 3] }],
49
+ sequentialRenders: [
50
+ { cond: true, foo: true, items: [1, 2, 3] },
51
+ { cond: true, foo: true, items: [1, 2, 3] },
52
+ { cond: true, foo: true, items: [1, 2, 3, 4] },
53
+ { cond: false, foo: true, items: [1, 2, 3] },
54
+ { cond: true, foo: false, items: [1, 2, 3] },
55
+ { cond: true, foo: true, items: null }, // errors because props.items.length throws
56
+ { cond: null, foo: true, items: [1] },
57
+ ],
58
+};
59
+
60
+```
61
+
62
+### Eval output
63
+(kind: ok) 3
64
+3
65
+4
66
+false
67
+false
68
+"error"
69
+null
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-expression.js
new
+24
@@ -0,0 +1,24 @@
1
+function Component(props) {
2
+ let result;
3
+ try {
4
+ // items.length is accessed WITHIN the && expression
5
+ result = props.cond && props.foo && props.items.length;
6
+ } catch (e) {
7
+ result = 'error';
8
+ }
9
+ return result;
10
+}
11
+
12
+export const FIXTURE_ENTRYPOINT = {
13
+ fn: Component,
14
+ params: [{cond: true, foo: true, items: [1, 2, 3]}],
15
+ sequentialRenders: [
16
+ {cond: true, foo: true, items: [1, 2, 3]},
17
+ {cond: true, foo: true, items: [1, 2, 3]},
18
+ {cond: true, foo: true, items: [1, 2, 3, 4]},
19
+ {cond: false, foo: true, items: [1, 2, 3]},
20
+ {cond: true, foo: false, items: [1, 2, 3]},
21
+ {cond: true, foo: true, items: null}, // errors because props.items.length throws
22
+ {cond: null, foo: true, items: [1]},
23
+ ],
24
+};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-multiple-value-blocks.expect.md
new
+163
@@ -0,0 +1,163 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component({a, b, cond, items}) {
6
+ try {
7
+ const x = a?.value;
8
+ // items.length is accessed WITHIN the ternary expression - throws if items is null
9
+ const y = cond ? b?.first : items.length;
10
+ const z = x && y;
11
+ return (
12
+ <div>
13
+ {String(x)}-{String(y)}-{String(z)}
14
+ </div>
15
+ );
16
+ } catch {
17
+ return <div>error</div>;
18
+ }
19
+}
20
+
21
+export const FIXTURE_ENTRYPOINT = {
22
+ fn: Component,
23
+ params: [
24
+ {
25
+ a: {value: 'A'},
26
+ b: {first: 'B1', second: 'B2'},
27
+ cond: true,
28
+ items: [1, 2, 3],
29
+ },
30
+ ],
31
+ sequentialRenders: [
32
+ {
33
+ a: {value: 'A'},
34
+ b: {first: 'B1', second: 'B2'},
35
+ cond: true,
36
+ items: [1, 2, 3],
37
+ },
38
+ {
39
+ a: {value: 'A'},
40
+ b: {first: 'B1', second: 'B2'},
41
+ cond: true,
42
+ items: [1, 2, 3],
43
+ },
44
+ {
45
+ a: {value: 'A'},
46
+ b: {first: 'B1', second: 'B2'},
47
+ cond: false,
48
+ items: [1, 2],
49
+ },
50
+ {a: null, b: {first: 'B1', second: 'B2'}, cond: true, items: [1, 2, 3]},
51
+ {a: {value: 'A'}, b: null, cond: true, items: [1, 2, 3]}, // b?.first is safe (returns undefined)
52
+ {a: {value: 'A'}, b: {first: 'B1', second: 'B2'}, cond: false, items: null}, // errors because items.length throws when cond=false
53
+ {
54
+ a: {value: ''},
55
+ b: {first: 'B1', second: 'B2'},
56
+ cond: true,
57
+ items: [1, 2, 3, 4],
58
+ },
59
+ ],
60
+};
61
+
62
+```
63
+
64
+## Code
65
+
66
+```javascript
67
+import { c as _c } from "react/compiler-runtime";
68
+function Component(t0) {
69
+ const $ = _c(5);
70
+ const { a, b, cond, items } = t0;
71
+ try {
72
+ const x = a?.value;
73
+
74
+ const y = cond ? b?.first : items.length;
75
+ const z = x && y;
76
+
77
+ const t1 = String(x);
78
+ const t2 = String(y);
79
+ const t3 = String(z);
80
+ let t4;
81
+ if ($[0] !== t1 || $[1] !== t2 || $[2] !== t3) {
82
+ t4 = (
83
+ <div>
84
+ {t1}-{t2}-{t3}
85
+ </div>
86
+ );
87
+ $[0] = t1;
88
+ $[1] = t2;
89
+ $[2] = t3;
90
+ $[3] = t4;
91
+ } else {
92
+ t4 = $[3];
93
+ }
94
+ return t4;
95
+ } catch {
96
+ let t1;
97
+ if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
98
+ t1 = <div>error</div>;
99
+ $[4] = t1;
100
+ } else {
101
+ t1 = $[4];
102
+ }
103
+ return t1;
104
+ }
105
+}
106
+
107
+export const FIXTURE_ENTRYPOINT = {
108
+ fn: Component,
109
+ params: [
110
+ {
111
+ a: { value: "A" },
112
+ b: { first: "B1", second: "B2" },
113
+ cond: true,
114
+ items: [1, 2, 3],
115
+ },
116
+ ],
117
+
118
+ sequentialRenders: [
119
+ {
120
+ a: { value: "A" },
121
+ b: { first: "B1", second: "B2" },
122
+ cond: true,
123
+ items: [1, 2, 3],
124
+ },
125
+ {
126
+ a: { value: "A" },
127
+ b: { first: "B1", second: "B2" },
128
+ cond: true,
129
+ items: [1, 2, 3],
130
+ },
131
+ {
132
+ a: { value: "A" },
133
+ b: { first: "B1", second: "B2" },
134
+ cond: false,
135
+ items: [1, 2],
136
+ },
137
+ { a: null, b: { first: "B1", second: "B2" }, cond: true, items: [1, 2, 3] },
138
+ { a: { value: "A" }, b: null, cond: true, items: [1, 2, 3] }, // b?.first is safe (returns undefined)
139
+ {
140
+ a: { value: "A" },
141
+ b: { first: "B1", second: "B2" },
142
+ cond: false,
143
+ items: null,
144
+ }, // errors because items.length throws when cond=false
145
+ {
146
+ a: { value: "" },
147
+ b: { first: "B1", second: "B2" },
148
+ cond: true,
149
+ items: [1, 2, 3, 4],
150
+ },
151
+ ],
152
+};
153
+
154
+```
155
+
156
+### Eval output
157
+(kind: ok) <div>A-B1-B1</div>
158
+<div>A-B1-B1</div>
159
+<div>A-2-2</div>
160
+<div>undefined-B1-undefined</div>
161
+<div>A-undefined-undefined</div>
162
+<div>error</div>
163
+<div>-B1-</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-multiple-value-blocks.js
new
+56
@@ -0,0 +1,56 @@
1
+function Component({a, b, cond, items}) {
2
+ try {
3
+ const x = a?.value;
4
+ // items.length is accessed WITHIN the ternary expression - throws if items is null
5
+ const y = cond ? b?.first : items.length;
6
+ const z = x && y;
7
+ return (
8
+ <div>
9
+ {String(x)}-{String(y)}-{String(z)}
10
+ </div>
11
+ );
12
+ } catch {
13
+ return <div>error</div>;
14
+ }
15
+}
16
+
17
+export const FIXTURE_ENTRYPOINT = {
18
+ fn: Component,
19
+ params: [
20
+ {
21
+ a: {value: 'A'},
22
+ b: {first: 'B1', second: 'B2'},
23
+ cond: true,
24
+ items: [1, 2, 3],
25
+ },
26
+ ],
27
+ sequentialRenders: [
28
+ {
29
+ a: {value: 'A'},
30
+ b: {first: 'B1', second: 'B2'},
31
+ cond: true,
32
+ items: [1, 2, 3],
33
+ },
34
+ {
35
+ a: {value: 'A'},
36
+ b: {first: 'B1', second: 'B2'},
37
+ cond: true,
38
+ items: [1, 2, 3],
39
+ },
40
+ {
41
+ a: {value: 'A'},
42
+ b: {first: 'B1', second: 'B2'},
43
+ cond: false,
44
+ items: [1, 2],
45
+ },
46
+ {a: null, b: {first: 'B1', second: 'B2'}, cond: true, items: [1, 2, 3]},
47
+ {a: {value: 'A'}, b: null, cond: true, items: [1, 2, 3]}, // b?.first is safe (returns undefined)
48
+ {a: {value: 'A'}, b: {first: 'B1', second: 'B2'}, cond: false, items: null}, // errors because items.length throws when cond=false
49
+ {
50
+ a: {value: ''},
51
+ b: {first: 'B1', second: 'B2'},
52
+ cond: true,
53
+ items: [1, 2, 3, 4],
54
+ },
55
+ ],
56
+};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nested-optional-chaining.expect.md
new
+104
@@ -0,0 +1,104 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component({data, fallback}) {
6
+ try {
7
+ // fallback.default is accessed WITHIN the optional chain via nullish coalescing
8
+ const value = data?.nested?.deeply?.value ?? fallback.default;
9
+ return <div>{value}</div>;
10
+ } catch {
11
+ return <div>error</div>;
12
+ }
13
+}
14
+
15
+export const FIXTURE_ENTRYPOINT = {
16
+ fn: Component,
17
+ params: [
18
+ {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}},
19
+ ],
20
+ sequentialRenders: [
21
+ {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}},
22
+ {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}},
23
+ {data: {nested: {deeply: {value: 'changed'}}}, fallback: {default: 'none'}},
24
+ {data: {nested: {deeply: null}}, fallback: {default: 'none'}}, // uses fallback.default
25
+ {data: {nested: null}, fallback: {default: 'none'}}, // uses fallback.default
26
+ {data: null, fallback: null}, // errors because fallback.default throws
27
+ {data: {nested: {deeply: {value: 42}}}, fallback: {default: 'none'}},
28
+ ],
29
+};
30
+
31
+```
32
+
33
+## Code
34
+
35
+```javascript
36
+import { c as _c } from "react/compiler-runtime";
37
+function Component(t0) {
38
+ const $ = _c(3);
39
+ const { data, fallback } = t0;
40
+ try {
41
+ const value = data?.nested?.deeply?.value ?? fallback.default;
42
+ let t1;
43
+ if ($[0] !== value) {
44
+ t1 = <div>{value}</div>;
45
+ $[0] = value;
46
+ $[1] = t1;
47
+ } else {
48
+ t1 = $[1];
49
+ }
50
+ return t1;
51
+ } catch {
52
+ let t1;
53
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
54
+ t1 = <div>error</div>;
55
+ $[2] = t1;
56
+ } else {
57
+ t1 = $[2];
58
+ }
59
+ return t1;
60
+ }
61
+}
62
+
63
+export const FIXTURE_ENTRYPOINT = {
64
+ fn: Component,
65
+ params: [
66
+ {
67
+ data: { nested: { deeply: { value: "found" } } },
68
+ fallback: { default: "none" },
69
+ },
70
+ ],
71
+
72
+ sequentialRenders: [
73
+ {
74
+ data: { nested: { deeply: { value: "found" } } },
75
+ fallback: { default: "none" },
76
+ },
77
+ {
78
+ data: { nested: { deeply: { value: "found" } } },
79
+ fallback: { default: "none" },
80
+ },
81
+ {
82
+ data: { nested: { deeply: { value: "changed" } } },
83
+ fallback: { default: "none" },
84
+ },
85
+ { data: { nested: { deeply: null } }, fallback: { default: "none" } }, // uses fallback.default
86
+ { data: { nested: null }, fallback: { default: "none" } }, // uses fallback.default
87
+ { data: null, fallback: null }, // errors because fallback.default throws
88
+ {
89
+ data: { nested: { deeply: { value: 42 } } },
90
+ fallback: { default: "none" },
91
+ },
92
+ ],
93
+};
94
+
95
+```
96
+
97
+### Eval output
98
+(kind: ok) <div>found</div>
99
+<div>found</div>
100
+<div>changed</div>
101
+<div>none</div>
102
+<div>none</div>
103
+<div>error</div>
104
+<div>42</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nested-optional-chaining.js
new
+25
@@ -0,0 +1,25 @@
1
+function Component({data, fallback}) {
2
+ try {
3
+ // fallback.default is accessed WITHIN the optional chain via nullish coalescing
4
+ const value = data?.nested?.deeply?.value ?? fallback.default;
5
+ return <div>{value}</div>;
6
+ } catch {
7
+ return <div>error</div>;
8
+ }
9
+}
10
+
11
+export const FIXTURE_ENTRYPOINT = {
12
+ fn: Component,
13
+ params: [
14
+ {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}},
15
+ ],
16
+ sequentialRenders: [
17
+ {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}},
18
+ {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}},
19
+ {data: {nested: {deeply: {value: 'changed'}}}, fallback: {default: 'none'}},
20
+ {data: {nested: {deeply: null}}, fallback: {default: 'none'}}, // uses fallback.default
21
+ {data: {nested: null}, fallback: {default: 'none'}}, // uses fallback.default
22
+ {data: null, fallback: null}, // errors because fallback.default throws
23
+ {data: {nested: {deeply: {value: 42}}}, fallback: {default: 'none'}},
24
+ ],
25
+};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nullish-coalescing.expect.md
new
+84
@@ -0,0 +1,84 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component({a, b, fallback}) {
6
+ try {
7
+ // fallback.value is accessed WITHIN the ?? chain
8
+ const result = a ?? b ?? fallback.value;
9
+ return <span>{result}</span>;
10
+ } catch {
11
+ return <span>error</span>;
12
+ }
13
+}
14
+
15
+export const FIXTURE_ENTRYPOINT = {
16
+ fn: Component,
17
+ params: [{a: 'first', b: 'second', fallback: {value: 'default'}}],
18
+ sequentialRenders: [
19
+ {a: 'first', b: 'second', fallback: {value: 'default'}},
20
+ {a: 'first', b: 'second', fallback: {value: 'default'}},
21
+ {a: null, b: 'second', fallback: {value: 'default'}},
22
+ {a: null, b: null, fallback: {value: 'fallback'}},
23
+ {a: undefined, b: undefined, fallback: {value: 'fallback'}},
24
+ {a: 0, b: 'not zero', fallback: {value: 'default'}},
25
+ {a: null, b: null, fallback: null}, // errors because fallback.value throws WITHIN the ?? chain
26
+ ],
27
+};
28
+
29
+```
30
+
31
+## Code
32
+
33
+```javascript
34
+import { c as _c } from "react/compiler-runtime";
35
+function Component(t0) {
36
+ const $ = _c(3);
37
+ const { a, b, fallback } = t0;
38
+ try {
39
+ const result = a ?? b ?? fallback.value;
40
+ let t1;
41
+ if ($[0] !== result) {
42
+ t1 = <span>{result}</span>;
43
+ $[0] = result;
44
+ $[1] = t1;
45
+ } else {
46
+ t1 = $[1];
47
+ }
48
+ return t1;
49
+ } catch {
50
+ let t1;
51
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
52
+ t1 = <span>error</span>;
53
+ $[2] = t1;
54
+ } else {
55
+ t1 = $[2];
56
+ }
57
+ return t1;
58
+ }
59
+}
60
+
61
+export const FIXTURE_ENTRYPOINT = {
62
+ fn: Component,
63
+ params: [{ a: "first", b: "second", fallback: { value: "default" } }],
64
+ sequentialRenders: [
65
+ { a: "first", b: "second", fallback: { value: "default" } },
66
+ { a: "first", b: "second", fallback: { value: "default" } },
67
+ { a: null, b: "second", fallback: { value: "default" } },
68
+ { a: null, b: null, fallback: { value: "fallback" } },
69
+ { a: undefined, b: undefined, fallback: { value: "fallback" } },
70
+ { a: 0, b: "not zero", fallback: { value: "default" } },
71
+ { a: null, b: null, fallback: null }, // errors because fallback.value throws WITHIN the ?? chain
72
+ ],
73
+};
74
+
75
+```
76
+
77
+### Eval output
78
+(kind: ok) <span>first</span>
79
+<span>first</span>
80
+<span>second</span>
81
+<span>fallback</span>
82
+<span>fallback</span>
83
+<span>0</span>
84
+<span>error</span>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nullish-coalescing.js
new
+23
@@ -0,0 +1,23 @@
1
+function Component({a, b, fallback}) {
2
+ try {
3
+ // fallback.value is accessed WITHIN the ?? chain
4
+ const result = a ?? b ?? fallback.value;
5
+ return <span>{result}</span>;
6
+ } catch {
7
+ return <span>error</span>;
8
+ }
9
+}
10
+
11
+export const FIXTURE_ENTRYPOINT = {
12
+ fn: Component,
13
+ params: [{a: 'first', b: 'second', fallback: {value: 'default'}}],
14
+ sequentialRenders: [
15
+ {a: 'first', b: 'second', fallback: {value: 'default'}},
16
+ {a: 'first', b: 'second', fallback: {value: 'default'}},
17
+ {a: null, b: 'second', fallback: {value: 'default'}},
18
+ {a: null, b: null, fallback: {value: 'fallback'}},
19
+ {a: undefined, b: undefined, fallback: {value: 'fallback'}},
20
+ {a: 0, b: 'not zero', fallback: {value: 'default'}},
21
+ {a: null, b: null, fallback: null}, // errors because fallback.value throws WITHIN the ?? chain
22
+ ],
23
+};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-call.expect.md
new
+132
@@ -0,0 +1,132 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component({obj, arg}) {
6
+ try {
7
+ // arg.value is accessed WITHIN the optional call expression as an argument
8
+ // When obj is non-null but arg is null, arg.value throws inside the optional chain
9
+ const result = obj?.method?.(arg.value);
10
+ return <span>{result ?? 'no result'}</span>;
11
+ } catch {
12
+ return <span>error</span>;
13
+ }
14
+}
15
+
16
+export const FIXTURE_ENTRYPOINT = {
17
+ fn: Component,
18
+ params: [{obj: {method: x => 'called:' + x}, arg: {value: 1}}],
19
+ sequentialRenders: [
20
+ {obj: {method: x => 'called:' + x}, arg: {value: 1}},
21
+ {obj: {method: x => 'called:' + x}, arg: {value: 1}},
22
+ {obj: {method: x => 'different:' + x}, arg: {value: 2}},
23
+ {obj: {method: null}, arg: {value: 3}},
24
+ {obj: {notMethod: true}, arg: {value: 4}},
25
+ {obj: null, arg: {value: 5}}, // obj is null, short-circuits so arg.value is NOT evaluated
26
+ {obj: {method: x => 'test:' + x}, arg: null}, // errors because arg.value throws WITHIN the optional call
27
+ ],
28
+};
29
+
30
+```
31
+
32
+## Code
33
+
34
+```javascript
35
+import { c as _c } from "react/compiler-runtime";
36
+function Component(t0) {
37
+ const $ = _c(6);
38
+ const { obj, arg } = t0;
39
+ try {
40
+ let t1;
41
+ if ($[0] !== arg || $[1] !== obj) {
42
+ t1 = obj?.method?.(arg.value);
43
+ $[0] = arg;
44
+ $[1] = obj;
45
+ $[2] = t1;
46
+ } else {
47
+ t1 = $[2];
48
+ }
49
+ const result = t1;
50
+ const t2 = result ?? "no result";
51
+ let t3;
52
+ if ($[3] !== t2) {
53
+ t3 = <span>{t2}</span>;
54
+ $[3] = t2;
55
+ $[4] = t3;
56
+ } else {
57
+ t3 = $[4];
58
+ }
59
+ return t3;
60
+ } catch {
61
+ let t1;
62
+ if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
63
+ t1 = <span>error</span>;
64
+ $[5] = t1;
65
+ } else {
66
+ t1 = $[5];
67
+ }
68
+ return t1;
69
+ }
70
+}
71
+
72
+export const FIXTURE_ENTRYPOINT = {
73
+ fn: Component,
74
+ params: [
75
+ {
76
+ obj: {
77
+ method: (x) => {
78
+ return "called:" + x;
79
+ },
80
+ },
81
+ arg: { value: 1 },
82
+ },
83
+ ],
84
+ sequentialRenders: [
85
+ {
86
+ obj: {
87
+ method: (x) => {
88
+ return "called:" + x;
89
+ },
90
+ },
91
+ arg: { value: 1 },
92
+ },
93
+ {
94
+ obj: {
95
+ method: (x) => {
96
+ return "called:" + x;
97
+ },
98
+ },
99
+ arg: { value: 1 },
100
+ },
101
+ {
102
+ obj: {
103
+ method: (x) => {
104
+ return "different:" + x;
105
+ },
106
+ },
107
+ arg: { value: 2 },
108
+ },
109
+ { obj: { method: null }, arg: { value: 3 } },
110
+ { obj: { notMethod: true }, arg: { value: 4 } },
111
+ { obj: null, arg: { value: 5 } }, // obj is null, short-circuits so arg.value is NOT evaluated
112
+ {
113
+ obj: {
114
+ method: (x) => {
115
+ return "test:" + x;
116
+ },
117
+ },
118
+ arg: null,
119
+ }, // errors because arg.value throws WITHIN the optional call
120
+ ],
121
+};
122
+
123
+```
124
+
125
+### Eval output
126
+(kind: ok) <span>called:1</span>
127
+<span>called:1</span>
128
+<span>different:2</span>
129
+<span>no result</span>
130
+<span>no result</span>
131
+<span>no result</span>
132
+<span>error</span>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-call.js
new
+24
@@ -0,0 +1,24 @@
1
+function Component({obj, arg}) {
2
+ try {
3
+ // arg.value is accessed WITHIN the optional call expression as an argument
4
+ // When obj is non-null but arg is null, arg.value throws inside the optional chain
5
+ const result = obj?.method?.(arg.value);
6
+ return <span>{result ?? 'no result'}</span>;
7
+ } catch {
8
+ return <span>error</span>;
9
+ }
10
+}
11
+
12
+export const FIXTURE_ENTRYPOINT = {
13
+ fn: Component,
14
+ params: [{obj: {method: x => 'called:' + x}, arg: {value: 1}}],
15
+ sequentialRenders: [
16
+ {obj: {method: x => 'called:' + x}, arg: {value: 1}},
17
+ {obj: {method: x => 'called:' + x}, arg: {value: 1}},
18
+ {obj: {method: x => 'different:' + x}, arg: {value: 2}},
19
+ {obj: {method: null}, arg: {value: 3}},
20
+ {obj: {notMethod: true}, arg: {value: 4}},
21
+ {obj: null, arg: {value: 5}}, // obj is null, short-circuits so arg.value is NOT evaluated
22
+ {obj: {method: x => 'test:' + x}, arg: null}, // errors because arg.value throws WITHIN the optional call
23
+ ],
24
+};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-chaining.expect.md
new
+84
@@ -0,0 +1,84 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Foo({json}) {
6
+ try {
7
+ const foo = JSON.parse(json)?.foo;
8
+ return <span>{foo}</span>;
9
+ } catch {
10
+ return null;
11
+ }
12
+}
13
+
14
+export const FIXTURE_ENTRYPOINT = {
15
+ fn: Foo,
16
+ params: [{json: '{"foo": "hello"}'}],
17
+ sequentialRenders: [
18
+ {json: '{"foo": "hello"}'},
19
+ {json: '{"foo": "hello"}'},
20
+ {json: '{"foo": "world"}'},
21
+ {json: '{"bar": "no foo"}'},
22
+ {json: '{}'},
23
+ {json: 'invalid json'},
24
+ {json: '{"foo": 42}'},
25
+ ],
26
+};
27
+
28
+```
29
+
30
+## Code
31
+
32
+```javascript
33
+import { c as _c } from "react/compiler-runtime";
34
+function Foo(t0) {
35
+ const $ = _c(4);
36
+ const { json } = t0;
37
+ try {
38
+ let t1;
39
+ if ($[0] !== json) {
40
+ t1 = JSON.parse(json)?.foo;
41
+ $[0] = json;
42
+ $[1] = t1;
43
+ } else {
44
+ t1 = $[1];
45
+ }
46
+ const foo = t1;
47
+ let t2;
48
+ if ($[2] !== foo) {
49
+ t2 = <span>{foo}</span>;
50
+ $[2] = foo;
51
+ $[3] = t2;
52
+ } else {
53
+ t2 = $[3];
54
+ }
55
+ return t2;
56
+ } catch {
57
+ return null;
58
+ }
59
+}
60
+
61
+export const FIXTURE_ENTRYPOINT = {
62
+ fn: Foo,
63
+ params: [{ json: '{"foo": "hello"}' }],
64
+ sequentialRenders: [
65
+ { json: '{"foo": "hello"}' },
66
+ { json: '{"foo": "hello"}' },
67
+ { json: '{"foo": "world"}' },
68
+ { json: '{"bar": "no foo"}' },
69
+ { json: "{}" },
70
+ { json: "invalid json" },
71
+ { json: '{"foo": 42}' },
72
+ ],
73
+};
74
+
75
+```
76
+
77
+### Eval output
78
+(kind: ok) <span>hello</span>
79
+<span>hello</span>
80
+<span>world</span>
81
+<span></span>
82
+<span></span>
83
+null
84
+<span>42</span>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-chaining.js
new
+22
@@ -0,0 +1,22 @@
1
+function Foo({json}) {
2
+ try {
3
+ const foo = JSON.parse(json)?.foo;
4
+ return <span>{foo}</span>;
5
+ } catch {
6
+ return null;
7
+ }
8
+}
9
+
10
+export const FIXTURE_ENTRYPOINT = {
11
+ fn: Foo,
12
+ params: [{json: '{"foo": "hello"}'}],
13
+ sequentialRenders: [
14
+ {json: '{"foo": "hello"}'},
15
+ {json: '{"foo": "hello"}'},
16
+ {json: '{"foo": "world"}'},
17
+ {json: '{"bar": "no foo"}'},
18
+ {json: '{}'},
19
+ {json: 'invalid json'},
20
+ {json: '{"foo": 42}'},
21
+ ],
22
+};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-ternary-expression.expect.md
new
+63
@@ -0,0 +1,63 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component(props) {
6
+ let result;
7
+ try {
8
+ // fallback.value is accessed WITHIN the ternary's false branch
9
+ result = props.cond ? props.a : props.fallback.value;
10
+ } catch (e) {
11
+ result = 'error';
12
+ }
13
+ return result;
14
+}
15
+
16
+export const FIXTURE_ENTRYPOINT = {
17
+ fn: Component,
18
+ params: [{cond: true, a: 'hello', fallback: {value: 'world'}}],
19
+ sequentialRenders: [
20
+ {cond: true, a: 'hello', fallback: {value: 'world'}},
21
+ {cond: true, a: 'hello', fallback: {value: 'world'}},
22
+ {cond: false, a: 'hello', fallback: {value: 'world'}},
23
+ {cond: true, a: 'foo', fallback: {value: 'bar'}},
24
+ {cond: false, a: 'foo', fallback: null}, // errors because fallback.value throws WITHIN the ternary
25
+ ],
26
+};
27
+
28
+```
29
+
30
+## Code
31
+
32
+```javascript
33
+function Component(props) {
34
+ let result;
35
+ try {
36
+ result = props.cond ? props.a : props.fallback.value;
37
+ } catch (t0) {
38
+ result = "error";
39
+ }
40
+
41
+ return result;
42
+}
43
+
44
+export const FIXTURE_ENTRYPOINT = {
45
+ fn: Component,
46
+ params: [{ cond: true, a: "hello", fallback: { value: "world" } }],
47
+ sequentialRenders: [
48
+ { cond: true, a: "hello", fallback: { value: "world" } },
49
+ { cond: true, a: "hello", fallback: { value: "world" } },
50
+ { cond: false, a: "hello", fallback: { value: "world" } },
51
+ { cond: true, a: "foo", fallback: { value: "bar" } },
52
+ { cond: false, a: "foo", fallback: null }, // errors because fallback.value throws WITHIN the ternary
53
+ ],
54
+};
55
+
56
+```
57
+
58
+### Eval output
59
+(kind: ok) "hello"
60
+"hello"
61
+"world"
62
+"foo"
63
+"error"
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-ternary-expression.js
new
+22
@@ -0,0 +1,22 @@
1
+function Component(props) {
2
+ let result;
3
+ try {
4
+ // fallback.value is accessed WITHIN the ternary's false branch
5
+ result = props.cond ? props.a : props.fallback.value;
6
+ } catch (e) {
7
+ result = 'error';
8
+ }
9
+ return result;
10
+}
11
+
12
+export const FIXTURE_ENTRYPOINT = {
13
+ fn: Component,
14
+ params: [{cond: true, a: 'hello', fallback: {value: 'world'}}],
15
+ sequentialRenders: [
16
+ {cond: true, a: 'hello', fallback: {value: 'world'}},
17
+ {cond: true, a: 'hello', fallback: {value: 'world'}},
18
+ {cond: false, a: 'hello', fallback: {value: 'world'}},
19
+ {cond: true, a: 'foo', fallback: {value: 'bar'}},
20
+ {cond: false, a: 'foo', fallback: null}, // errors because fallback.value throws WITHIN the ternary
21
+ ],
22
+};