Skip to content

Commit 1b16995

Browse files
committed
[compiler][wip] Support finally clauses
First pass at adding support for `finally` clauses. I took a simple approach where we reify the JS semantics around return shadowing in the HIR and output. Ie, we create a temporary variable that will be the return value, and change returns in the try/catch to assign to that value and then break to the finalizer. In the finalizer returns work as normal, but we put a final extra "if temporary set, return it" statement. It would be more ideal to fully restore the return shadowing, but given that finally clauses are relatively rare this seems like a reasonable compromise. I need to do more analysis to make sure the approach is correct.
1 parent 3a01c6f commit 1b16995

File tree

52 files changed

+1724
-339
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1724
-339
lines changed

compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts

Lines changed: 81 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,85 +1329,109 @@ function lowerStatement(
13291329
const continuationBlock = builder.reserve('block');
13301330

13311331
const handlerPath = stmt.get('handler');
1332-
if (!hasNode(handlerPath)) {
1332+
const finalizerPath = stmt.get('finalizer');
1333+
1334+
// Must have at least a catch or finally clause
1335+
if (!hasNode(handlerPath) && !hasNode(finalizerPath)) {
13331336
builder.errors.push({
1334-
reason: `(BuildHIR::lowerStatement) Handle TryStatement without a catch clause`,
1337+
reason: `(BuildHIR::lowerStatement) Handle TryStatement without a catch or finally clause`,
13351338
category: ErrorCategory.Todo,
13361339
loc: stmt.node.loc ?? null,
13371340
suggestions: null,
13381341
});
13391342
return;
13401343
}
1341-
if (hasNode(stmt.get('finalizer'))) {
1342-
builder.errors.push({
1343-
reason: `(BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause`,
1344-
category: ErrorCategory.Todo,
1345-
loc: stmt.node.loc ?? null,
1346-
suggestions: null,
1344+
1345+
// Build finalizer (finally) block first, so we know its ID for control flow
1346+
let finalizer: BlockId | null = null;
1347+
if (hasNode(finalizerPath)) {
1348+
finalizer = builder.enter('block', _blockId => {
1349+
lowerStatement(builder, finalizerPath);
1350+
return {
1351+
kind: 'goto',
1352+
block: continuationBlock.id,
1353+
variant: GotoVariant.Break,
1354+
id: makeInstructionId(0),
1355+
loc: finalizerPath.node.loc ?? GeneratedSource,
1356+
};
13471357
});
13481358
}
13491359

1350-
const handlerBindingPath = handlerPath.get('param');
1360+
// Determine the target for try/catch block exits
1361+
// If there's a finalizer, go there first; otherwise go to continuation
1362+
const afterTryCatchTarget = finalizer ?? continuationBlock.id;
1363+
1364+
// Build handler (catch) block if present
1365+
let handler: BlockId | null = null;
13511366
let handlerBinding: {
13521367
place: Place;
13531368
path: NodePath<t.Identifier | t.ArrayPattern | t.ObjectPattern>;
13541369
} | null = null;
1355-
if (hasNode(handlerBindingPath)) {
1356-
const place: Place = {
1357-
kind: 'Identifier',
1358-
identifier: builder.makeTemporary(
1359-
handlerBindingPath.node.loc ?? GeneratedSource,
1360-
),
1361-
effect: Effect.Unknown,
1362-
reactive: false,
1363-
loc: handlerBindingPath.node.loc ?? GeneratedSource,
1364-
};
1365-
promoteTemporary(place.identifier);
1366-
lowerValueToTemporary(builder, {
1367-
kind: 'DeclareLocal',
1368-
lvalue: {
1369-
kind: InstructionKind.Catch,
1370-
place: {...place},
1371-
},
1372-
type: null,
1373-
loc: handlerBindingPath.node.loc ?? GeneratedSource,
1374-
});
13751370

1376-
handlerBinding = {
1377-
path: handlerBindingPath,
1378-
place,
1379-
};
1380-
}
1371+
if (hasNode(handlerPath)) {
1372+
const handlerBindingPath = handlerPath.get('param');
1373+
if (hasNode(handlerBindingPath)) {
1374+
const place: Place = {
1375+
kind: 'Identifier',
1376+
identifier: builder.makeTemporary(
1377+
handlerBindingPath.node.loc ?? GeneratedSource,
1378+
),
1379+
effect: Effect.Unknown,
1380+
reactive: false,
1381+
loc: handlerBindingPath.node.loc ?? GeneratedSource,
1382+
};
1383+
promoteTemporary(place.identifier);
1384+
lowerValueToTemporary(builder, {
1385+
kind: 'DeclareLocal',
1386+
lvalue: {
1387+
kind: InstructionKind.Catch,
1388+
place: {...place},
1389+
},
1390+
type: null,
1391+
loc: handlerBindingPath.node.loc ?? GeneratedSource,
1392+
});
13811393

1382-
const handler = builder.enter('catch', _blockId => {
1383-
if (handlerBinding !== null) {
1384-
lowerAssignment(
1385-
builder,
1386-
handlerBinding.path.node.loc ?? GeneratedSource,
1387-
InstructionKind.Catch,
1388-
handlerBinding.path,
1389-
{...handlerBinding.place},
1390-
'Assignment',
1391-
);
1394+
handlerBinding = {
1395+
path: handlerBindingPath,
1396+
place,
1397+
};
13921398
}
1393-
lowerStatement(builder, handlerPath.get('body'));
1394-
return {
1395-
kind: 'goto',
1396-
block: continuationBlock.id,
1397-
variant: GotoVariant.Break,
1398-
id: makeInstructionId(0),
1399-
loc: handlerPath.node.loc ?? GeneratedSource,
1400-
};
1401-
});
1399+
1400+
handler = builder.enter('catch', _blockId => {
1401+
if (handlerBinding !== null) {
1402+
lowerAssignment(
1403+
builder,
1404+
handlerBinding.path.node.loc ?? GeneratedSource,
1405+
InstructionKind.Catch,
1406+
handlerBinding.path,
1407+
{...handlerBinding.place},
1408+
'Assignment',
1409+
);
1410+
}
1411+
lowerStatement(builder, handlerPath.get('body'));
1412+
return {
1413+
kind: 'goto',
1414+
block: afterTryCatchTarget,
1415+
variant: GotoVariant.Break,
1416+
id: makeInstructionId(0),
1417+
loc: handlerPath.node.loc ?? GeneratedSource,
1418+
};
1419+
});
1420+
}
14021421

14031422
const block = builder.enter('block', _blockId => {
14041423
const block = stmt.get('block');
1405-
builder.enterTryCatch(handler, () => {
1424+
// If there's a handler, exceptions go there; otherwise they propagate normally
1425+
if (handler !== null) {
1426+
builder.enterTryCatch(handler, () => {
1427+
lowerStatement(builder, block);
1428+
});
1429+
} else {
14061430
lowerStatement(builder, block);
1407-
});
1431+
}
14081432
return {
14091433
kind: 'goto',
1410-
block: continuationBlock.id,
1434+
block: afterTryCatchTarget,
14111435
variant: GotoVariant.Try,
14121436
id: makeInstructionId(0),
14131437
loc: block.node.loc ?? GeneratedSource,
@@ -1421,6 +1445,7 @@ function lowerStatement(
14211445
handlerBinding:
14221446
handlerBinding !== null ? {...handlerBinding.place} : null,
14231447
handler,
1448+
finalizer,
14241449
fallthrough: continuationBlock.id,
14251450
id: makeInstructionId(0),
14261451
loc: stmt.node.loc ?? GeneratedSource,

compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,8 @@ export type ReactiveTryTerminal = {
273273
kind: 'try';
274274
block: ReactiveBlock;
275275
handlerBinding: Place | null;
276-
handler: ReactiveBlock;
276+
handler: ReactiveBlock | null;
277+
finalizer: ReactiveBlock | null;
277278
id: InstructionId;
278279
loc: SourceLocation;
279280
};
@@ -602,8 +603,8 @@ export type TryTerminal = {
602603
kind: 'try';
603604
block: BlockId;
604605
handlerBinding: Place | null;
605-
handler: BlockId;
606-
// TODO: support `finally`
606+
handler: BlockId | null;
607+
finalizer: BlockId | null;
607608
fallthrough: BlockId;
608609
id: InstructionId;
609610
loc: SourceLocation;

compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,7 @@ export function removeUnnecessaryTryCatch(fn: HIR): void {
954954
for (const [, block] of fn.blocks) {
955955
if (
956956
block.terminal.kind === 'try' &&
957+
block.terminal.handler !== null &&
957958
!fn.blocks.has(block.terminal.handler)
958959
) {
959960
const handlerId = block.terminal.handler;

compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,12 +310,14 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
310310
break;
311311
}
312312
case 'try': {
313-
value = `[${terminal.id}] Try block=bb${terminal.block} handler=bb${
314-
terminal.handler
313+
value = `[${terminal.id}] Try block=bb${terminal.block}${
314+
terminal.handler !== null ? ` handler=bb${terminal.handler}` : ''
315315
}${
316316
terminal.handlerBinding !== null
317317
? ` handlerBinding=(${printPlace(terminal.handlerBinding)})`
318318
: ''
319+
}${
320+
terminal.finalizer !== null ? ` finalizer=bb${terminal.finalizer}` : ''
319321
} fallthrough=${
320322
terminal.fallthrough != null ? `bb${terminal.fallthrough}` : ''
321323
}`;

compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -921,13 +921,17 @@ export function mapTerminalSuccessors(
921921
}
922922
case 'try': {
923923
const block = fn(terminal.block);
924-
const handler = fn(terminal.handler);
924+
const handler =
925+
terminal.handler !== null ? fn(terminal.handler) : null;
926+
const finalizer =
927+
terminal.finalizer !== null ? fn(terminal.finalizer) : null;
925928
const fallthrough = fn(terminal.fallthrough);
926929
return {
927930
kind: 'try',
928931
block,
929932
handlerBinding: terminal.handlerBinding,
930933
handler,
934+
finalizer,
931935
fallthrough,
932936
id: makeInstructionId(0),
933937
loc: terminal.loc,
@@ -1088,6 +1092,12 @@ export function* eachTerminalSuccessor(terminal: Terminal): Iterable<BlockId> {
10881092
}
10891093
case 'try': {
10901094
yield terminal.block;
1095+
if (terminal.handler !== null) {
1096+
yield terminal.handler;
1097+
}
1098+
if (terminal.finalizer !== null) {
1099+
yield terminal.finalizer;
1100+
}
10911101
break;
10921102
}
10931103
case 'scope':

compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,11 @@ function inferBlock(
522522
instr.effects = effects;
523523
}
524524
const terminal = block.terminal;
525-
if (terminal.kind === 'try' && terminal.handlerBinding != null) {
525+
if (
526+
terminal.kind === 'try' &&
527+
terminal.handler !== null &&
528+
terminal.handlerBinding != null
529+
) {
526530
context.catchHandlers.set(terminal.handler, terminal.handlerBinding);
527531
} else if (terminal.kind === 'maybe-throw') {
528532
const handlerParam = context.catchHandlers.get(terminal.handler);

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -844,14 +844,33 @@ class Driver {
844844
const scheduleId = this.cx.schedule(fallthroughId, 'if');
845845
scheduleIds.push(scheduleId);
846846
}
847-
this.cx.scheduleCatchHandler(terminal.handler);
847+
if (terminal.handler !== null) {
848+
this.cx.scheduleCatchHandler(terminal.handler);
849+
}
850+
let finalizerScheduleId: number | null = null;
851+
if (terminal.finalizer !== null) {
852+
finalizerScheduleId = this.cx.schedule(terminal.finalizer, 'if');
853+
scheduleIds.push(finalizerScheduleId);
854+
}
848855

849856
const block = this.traverseBlock(
850857
this.cx.ir.blocks.get(terminal.block)!,
851858
);
852-
const handler = this.traverseBlock(
853-
this.cx.ir.blocks.get(terminal.handler)!,
854-
);
859+
const handler =
860+
terminal.handler !== null
861+
? this.traverseBlock(this.cx.ir.blocks.get(terminal.handler)!)
862+
: null;
863+
864+
// Unschedule finalizer before traversing it, so its own goto
865+
// doesn't become a break to itself
866+
if (finalizerScheduleId !== null) {
867+
this.cx.unschedule(finalizerScheduleId);
868+
scheduleIds.pop();
869+
}
870+
const finalizer =
871+
terminal.finalizer !== null
872+
? this.traverseBlock(this.cx.ir.blocks.get(terminal.finalizer)!)
873+
: null;
855874

856875
this.cx.unscheduleAll(scheduleIds);
857876
blockValue.push({
@@ -864,6 +883,7 @@ class Driver {
864883
block,
865884
handlerBinding: terminal.handlerBinding,
866885
handler,
886+
finalizer,
867887
id: terminal.id,
868888
},
869889
});

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,15 +1307,26 @@ function codegenTerminal(
13071307
return codegenBlock(cx, terminal.block);
13081308
}
13091309
case 'try': {
1310-
let catchParam = null;
1311-
if (terminal.handlerBinding !== null) {
1312-
catchParam = convertIdentifier(terminal.handlerBinding.identifier);
1313-
cx.temp.set(terminal.handlerBinding.identifier.declarationId, null);
1310+
let catchClause: t.CatchClause | null = null;
1311+
if (terminal.handler !== null) {
1312+
let catchParam = null;
1313+
if (terminal.handlerBinding !== null) {
1314+
catchParam = convertIdentifier(terminal.handlerBinding.identifier);
1315+
cx.temp.set(terminal.handlerBinding.identifier.declarationId, null);
1316+
}
1317+
catchClause = t.catchClause(catchParam, codegenBlock(cx, terminal.handler));
13141318
}
1319+
1320+
const finalizer =
1321+
terminal.finalizer !== null
1322+
? codegenBlock(cx, terminal.finalizer)
1323+
: null;
1324+
13151325
return createTryStatement(
13161326
terminal.loc,
13171327
codegenBlock(cx, terminal.block),
1318-
t.catchClause(catchParam, codegenBlock(cx, terminal.handler)),
1328+
catchClause,
1329+
finalizer,
13191330
);
13201331
}
13211332
default: {

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -395,14 +395,24 @@ function writeTerminal(writer: Writer, terminal: ReactiveTerminal): void {
395395
case 'try': {
396396
writer.writeLine(`[${terminal.id}] try {`);
397397
writeReactiveInstructions(writer, terminal.block);
398-
writer.write(`} catch `);
399-
if (terminal.handlerBinding !== null) {
400-
writer.writeLine(`(${printPlace(terminal.handlerBinding)}) {`);
401-
} else {
402-
writer.writeLine(`{`);
398+
if (terminal.handler !== null) {
399+
writer.write(`} catch `);
400+
if (terminal.handlerBinding !== null) {
401+
writer.writeLine(`(${printPlace(terminal.handlerBinding)}) {`);
402+
} else {
403+
writer.writeLine(`{`);
404+
}
405+
writeReactiveInstructions(writer, terminal.handler);
406+
writer.writeLine('}');
407+
}
408+
if (terminal.finalizer !== null) {
409+
writer.writeLine(`} finally {`);
410+
writeReactiveInstructions(writer, terminal.finalizer);
411+
writer.writeLine('}');
412+
}
413+
if (terminal.handler === null && terminal.finalizer === null) {
414+
writer.writeLine('}');
403415
}
404-
writeReactiveInstructions(writer, terminal.handler);
405-
writer.writeLine('}');
406416
break;
407417
}
408418
default:

0 commit comments

Comments
 (0)