fix(cfg): synthesize a protected block for an empty Kotlin try {} (#2195)

An empty `try {}` body produced zero protected blocks, so visitTry's throw-edge
loop wired nothing to the catch and the try's entry fell through to the finally
— leaving the catch handler block + its error binding orphaned (unreachable from
ENTRY), a malformed CFG with stranded def/use facts. Mirror the existing
empty-`catch` synthesis: when the try body is empty and there is a catch or
finally, synthesize one protected block so the catch handler(s) are wired and
the try entry is the body, not the finally. Found by the per-language CFG
verification swarm. Non-empty try is byte-identical; kotlin suite 31 passed,
tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-14 21:21:26 +00:00
parent 5d284b8f5f
commit 6fed52e1a6
2 changed files with 23 additions and 1 deletions

View file

@ -739,9 +739,19 @@ class KotlinCfgWalk {
const tryHandler = firstCatchEntry ?? finalizerEntry ?? this.currentHandler();
const protectedStart = this.builder.blockCount;
this.handlers.push(tryHandler);
const bodyRes = bodyNode ? this.visitSeq(this.statementsOf(bodyNode)) : null;
let bodyRes = bodyNode ? this.visitSeq(this.statementsOf(bodyNode)) : null;
this.handlers.pop();
if (bodyRes === null && (catchBlocks.length > 0 || finalizerEntry !== undefined)) {
// An empty `try {}` body still establishes a protected region. Synthesize
// one block (like the empty-`catch` case above) so the throw-edge loop
// wires the catch handler(s) and the try's entry is the body — otherwise
// the catch handler block + its error binding are orphaned (unreachable
// from ENTRY) and control routes straight to the finally, bypassing catch.
const idx = this.builder.newBlock(startLineOf(stmt), startLineOf(stmt), '');
bodyRes = { entry: idx, exits: [idx] };
}
if (catchBlocks.length > 0 || finalizerEntry !== undefined) {
for (let b = protectedStart; b < this.builder.blockCount; b++) {
this.builder.edge(b, tryHandler, 'throw');

View file

@ -239,6 +239,18 @@ describe('Kotlin CfgVisitor — try/catch/finally', () => {
expect(reaches(cfg, thr, block(cfg, 'done()'))).toBe(false);
expect(reachable(cfg, block(cfg, 'done()'))).toBe(true); // via the if false branch
});
it('empty try {} with catch + finally keeps the catch handler reachable (#2195)', () => {
// An empty try body still establishes a protected region — the catch (and
// its error binding) must not be orphaned/unreachable from ENTRY.
const cfg = kotlin.cfgOf(
`fun f() { try {} catch (e: Exception) { handle(e) } finally { cl() }; a() }`,
);
expect(reachable(cfg, block(cfg, 'handle(e)'))).toBe(true);
expect(reachable(cfg, block(cfg, 'cl()'))).toBe(true);
expect(reachable(cfg, block(cfg, 'a()'))).toBe(true);
expect(definesBinding(cfg, bindingIdx(cfg, 'e'))).toBe(true);
});
});
describe('Kotlin CfgVisitor — labeled break/continue', () => {