Related to BOO-24:
We must allow using "yield" inside the try part of try-except blocks.
C# allows to do this:
IEnumerable<string> ReadFile(string filename) {
using (StreamReader r = new StreamReader()) {
string line;
while ((line = r.ReadLine()) != null)
yield return line;
}
}
When the file is completely enumerated, the StreamReader will be closed,
and when a foreach loop is aborted (with "break" or exception), it will
call Dispose() on the generated enumerator which also has the effect of
closing the StreamReader.
In Boo, it is currently not possible to get the same behavior without
writing the enumerator manually. And even if one does that, it doesn't
work correctly unless "for" calls Dispose(). And because "for" needs to
use a try-ensure block for calling Dispose(), and there's lots of code
with "yield" inside "for", it is important to support "yield" inside "try".
Otherwise using LINQ becomes a huge PITA as soon as you're using any
LINQ implementation that needs to dispose something (files, SQL
connections, ...) - basically anything except the plain LINQ-to-Objects.
We also need to ensure that all Boo.Lang code that manually consumes
enumerators (e.g. the builtin functions like zip) call Dispose(). When
possible, these should be rewritten to use foreach and yield return so
that the C# compiler takes care of forwarding the Dispose() calls.
Note that the C# 3.0 compiler implements this as follows (I think C# 2.0
did something different):
The body of MoveNext is wrapped inside a huge try-fault block:
try { /* usual method body */ } fault { this.Dispose(); }
try-fault is pseudo-C# for the IL fault handler, which works just like a
finally handler except it is only called when the block is left due to
an exception, not when it is left by a normal return statement.
(Boo supports this directly, it's try-failure)
Remember the cases in which the "ensure" code should run:
1) when control flow in the generator normally leaves the "try" block
(but not when control flow leaves the "try" block due to "yield" being
implemented as "return Yield(...)")
2) when the generator code causes an exception in the "try" block
3) when the enumeration is aborted (due to "break" or exception in the
for loop) - in this case, Dispose() is called on the enumerator.
Because a yield statement is invalid in try-except (C# allows it only in
the try block of try-finally, not in the finally handler itself, and not
in try-catch blocks), an exception (no matter whether inside the
enumerator or inside the consumer loop) will always cause the
enumeration to finish, calling all outstanding finally blocks. The
try-fault block causes case 2 and case 3 behave identically, the
Dispose() method will handle executing the finally blocks surrounding
the current yield. Note that this means that we have to update the state
index when entering and leaving try blocks, not only on the "yield" call
itself. The C# compiler handles this by having multiple kind of states,
"running" and "suspended" states. There's a global "running" state
(which is also used as finish state), and a "running" state per
try-ensure statement, so that the Dispose() method always knows which
ensure blocks must be executed in case of an exception that causes
Dispose() to be called through the fault handler.
The "suspended" states are set immediately before returning from
MoveNext so that the next call knows where to continue; Dispose() also
knows for which finally blocks must be run for "suspended" states.
This handles cases 2) and 3) correctly. Case 1) is much simpler, at the
end of try blocks the state is set to the running state of the next
outer try block and the finally code is run.
All try-finally constructs containing "yield" were now replaced by
normal code (state setting and running the finally code normally), plus
a single try-fault handler. But because that fault handler is around the
whole method, the state machine doesn't have to jump into try blocks
anymore (this was mentioned as a problem in BOO-24).
Hmm.. this sounds interesting, I think I'll try to implement it myself ;-)
Daniel