Repository navigation
A return, break or continue through a finally disposes and releases the scopes the finally reads after it runs - #600
Merged
Conversation
…he scopes the finally reads after it runs A `return` disposed the `using`s around a try before its finally ran, and a `break` or `continue` out of a try disposed and released the scopes around it up to its loop before the finally ran (under -mm=rc the finally read freed strings). Both walks now stop at the try's finallyGuardScope; the finally ends with one `ts.If` per place a jump goes, tagged return-only or jump-only by kind and label, and TryOpLowering keeps each in its own copy, unwrapped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77
added a commit
that referenced
this pull request
Oct 23, 2026
* rc: a temporary a finally reads is released after the finally
`const v = mv(); try { if (c) continue; } finally { s += v.get(); }`: a
folded const (a call's result named by a const) is released where its
block ends, and in front of each break, continue or return that leaves
the block early. That release ran before the finally the jump goes
through, and the finally then read a freed block. #600 fixed the same
for locals in slots; a folded const is released by
releaseDiscardedTemporaries instead.
When a finally between the jump and the temporary's block reads it, the
release now goes at the end of the outermost such finally, in a ts.If
tagged with the jump (JUMP_ONLY_ATTR_NAME, RETURN_ONLY_ATTR_NAME), so
TryOpLowering keeps it in that jump's copy of the finally alone - the
scheme MLIRGen uses for the scopes the jump leaves. Jumps with one key
share one copy and get one release. Fuzzer (#646) R6, rc.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* rc: a jump through a finally that jumps itself releases a temporary once
`for (..) { const v = mv(); try { try { continue; } finally { break; } }
finally {} }`: the temporary was released in front of the `continue`,
and again in front of the `break` its finally then took. The release
for a jump that passes through a finally now always goes at the end of
the outermost such finally, tagged with the jump, not only when the
finally reads the temporary: a finally that leaves by a jump of its own
never reaches it, and the jump's own release is then the only one.
Fuzzer (#646), found past the first 1000 programs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follows #598, using the same design for disposal and for
break/continue.Bugs
returndisposed too early.{ using r = new R(); try { return 1; } finally { r.read(); } }disposedrbefore the finally ran, in every memory model. JS disposes it after the finally. The cause:mlirGenDisposablehad no finally guard.break/continuereleased and disposed too early. A jump out of a try in a loop body, or in a block nested in it, disposed and released the scopes around the try up to its loop before it went through the finally copy. Under-mm=rcthe finally then read freed strings.Fix
scopeExitStopsAtFinallyGuard: both walks, dispose and release, now stop atfinallyGuardScopeforFullStack,BreakScopeandLoopScope. A break or continue notes(kind, label)inGenContext::finallyEscapingJumps, which the body guard and the catch guard share.mlirGenScopeExitAfterFinallyreplacesmlirGenReleaseOwnedOnReturn. At the end of the finally it adds onets.If truefor the return and one for each escaping jump. Each holds the dispose and release of the scopes from the try outwards, using that jump's own walk. Each is taggedRETURN_ONLY_ATTR_NAME, orJUMP_ONLY_ATTR_NAME = "break:<label>"/"continue:<label>".IfOp'sRemoveStaticConditionskips tagged ifs. Otherwise the canonicalizer that runs before lowering inlines them and the tag is lost.breakand abreak outercan share a target and still each get their own scope exit. The group that belongs to a copy is unwrapped (inlineBlockBefore); all other groups are erased. If the wrapper were left in place it would lower to a constantcond_br, and the ownership verifier would see its other edge as a path that never releases.Tests
00try_scope_exit_through_finally.tsis registered for compile, JIT andTSLANG_CORPUS. It covers:usingin a block, in a catch, and through two finallysusingcontinue outerValues are read after
churn().--emit=mlir-affine --verify-ownership -mm=rc: quiet on every test that contains afinally, including the new one.ctest -C Release -j 8 --timeout 300: 100% passed, 0 of 4156 failed.Not fixed (separate, existing bug)
On Windows, a
breakout of a catch clause crashes (0x80000003) in both JIT and AOT, under gc and none, with or without a finally:for (...) { try { h(); } catch { if (i == 1) break; } }. The test leaves this case out.🤖 Generated with Claude Code