Skip to content

Fix #836: iterator lowering supports try/finally around yield - #846

Merged
DavidObando merged 1 commit into
mainfrom
copilot/issue-836
Jun 14, 2026
Merged

Fix #836: iterator lowering supports try/finally around yield#846
DavidObando merged 1 commit into
mainfrom
copilot/issue-836

Conversation

@DavidObando

Copy link
Copy Markdown
Owner

Summary

Iterator (yield) bodies now bind and lower correctly with try { … } finally { … } around yield, and the C#-forbidden try { yield … } catch (…) { … } combination is rejected with a clean diagnostic (GS0367). Closes the #806 workaround in the Gsharp.Extensions.Sequences stdlib.

Root cause

The end-to-end iterator lowering for try/finally was already implemented in IteratorMoveNextBodyBuilder + IteratorTryDispatchPlanner (landed under #419). What was missing was the binder-level rejection of try/catch around yield — the previous behaviour silently dropped the catch clause during lowering, which violates ECMA-335 §I.12 / C# §15.14 (yield cannot resume into a protected region that doubles as a CLR exception-handler frame).

The Gsharp.Extensions.Sequences stdlib then had to flatten WindowedIterator / IndexedIterator (the C# baseline shape used enumerator + try/finally + Dispose) when ported during #806, because there was no positive signal that try/finally + yield actually worked.

Fix

Binder (StatementBinder.BindTryStatement):

  • New diagnostic GS0367 raised when a yield lexically inside a try block coincides with one or more catch clauses on the same try. Fires once per offending yield.
  • The walker descends only the try block, skipping catches/finallies and FunctionLiteralExpression bodies — yields inside nested lambdas do not trip the rejection.

SDK (Gsharp.Extensions.Sequences.SequenceExtensions):

  • WindowedIterator[T] and IndexedIterator[T] restored to the explicit GetEnumerator + try/finally + Dispose shape that matches the C# baseline. InterleaveIterator's using let comment refreshed.

Tests

  • Binder (7 tests, Issue836IteratorTryFinallyBinderTests): try/finally accepted; try/catch rejected; try/catch/finally rejected; nested try/finally accepted; inner-catch-with-yield rejected; non-iterator try/catch unaffected; nested-lambda yield inside outer try-with-only-finally accepted.
  • Emit + IL-verify (4 tests, Issue836IteratorTryFinallyEmitTests): full enumeration runs the finally exactly once; early break runs the finally on Dispose; nested early-break runs inner-then-outer finally; finally runs exactly once on normal completion. All pass dotnet-ilverify (net10.0).
  • Interpreter (3 tests, Issue836IteratorTryFinallyInterpreterTests): tree-walking interpreter parity for try/finally + yield, including nested.
  • Existing IteratorTryFinallyEmitTests.Iterator_YieldInsideTryCatchFinally_* repurposed to assert GS0367 fires (previously asserted the catch was silently dropped).

Full dotnet test GSharp.sln suite green (5345 tests). Website (npm run build) builds cleanly with no broken links. ilverify clean on every emit fixture.

Follow-ups

None deferred — the implemented scope covers the issue requirements:

  • try/finally around yield (full + nested) lowers to verifiable IL.
  • try/catch around yield is rejected by the binder.
  • The Gsharp.Extensions.Sequences stdlib uses the restored shape.

Refs

Closes #836
Related #806, ADR-0084 §L5, parent #706

…in iterators (#836)

Iterator (yield) bodies in G# already support try/finally lowering
end-to-end (IteratorMoveNextBodyBuilder + IteratorTryDispatchPlanner,
landed under #419). What was missing was the binder-side rejection of
the C#-forbidden combination 'try { yield … } catch (…) { … }' and the
follow-through in the Gsharp.Extensions.Sequences stdlib, where #806
had to work around the missing diagnostic by hoisting cleanup outside
the iterator.

Binder:
* GS0367 (new): a yield lexically inside a try block that also has any
  catch clause is rejected (C# §15.14 / ECMA-335). The diagnostic fires
  once per offending yield, with the yield keyword's location. Pure
  try/finally around yield continues to bind cleanly; the iterator
  rewriter's existing 'drop catch clauses' fallback now becomes
  defense-in-depth.
* The walker descends only the try block (not catches/finallies, not
  nested FunctionLiteralExpression bodies), so a yield inside a nested
  lambda inside the try does not trip the diagnostic.

SDK:
* WindowedIterator[T] and IndexedIterator[T] in
  Gsharp.Extensions.Sequences now use the explicit
  GetEnumerator + try/finally + Dispose shape, matching the C#
  baseline that #806 had to flatten. Their tests (104 in
  Extensions.Tests) continue to pass; consumers that break out early
  now observe deterministic enumerator disposal.
* The InterleaveIterator comment is refreshed — using-let stays as
  the declarative shape but is no longer the only option.

Tests:
* Issue836IteratorTryFinallyBinderTests (Core.Tests): 7 binder cases
  covering try/finally accepted, try/catch rejected, try/catch/finally
  rejected, nested try/finally accepted, inner-catch-with-yield
  rejected, non-iterator try/catch unaffected, and yield-in-nested-
  lambda-inside-outer-try-with-only-finally accepted.
* Issue836IteratorTryFinallyEmitTests (Compiler.Tests): 4 end-to-end
  emit + ilverify-checked tests — full enumeration runs the finally
  exactly once, early break runs the finally on Dispose, nested
  early-break runs inner then outer finally.
* Issue836IteratorTryFinallyInterpreterTests (Interpreter.Tests): 3
  tree-walking interpreter parity tests for try/finally + yield;
  asserts every value is observed and each finally runs exactly once.
* IteratorTryFinallyEmitTests.Iterator_YieldInsideTryCatchFinally_*
  is repurposed to assert GS0367 fires (previously asserted the catch
  was silently dropped).

Full GSharp.sln test suite (5345 tests) green; dotnet-ilverify clean
on the new emit fixtures; website builds without broken links.

Closes #836
Related #806, ADR-0084 §L5, parent #706
@DavidObando
DavidObando merged commit ce12c20 into main Jun 14, 2026
7 checks passed
@DavidObando
DavidObando deleted the copilot/issue-836 branch June 14, 2026 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Iterator lowering rejects try/finally around yield

2 participants