It seems like what is really wanted is a way to clean up a scope when it closes, so it seems like we should just do that. Create a new block frame, such as "defer ... end" or "exiting ... end". Everything that is in scope at the point where the block begins is in scope throughout the block. However, the contents of the block are not executed inline; rather, once execution reaches the point in the code where the block begins, a deferred-execution guarantee is made for the contained code, which will be executed when the scope immediately containing the exiting ... end block is exited (in newest-guarantee-first order).
Isn't it functionally the same as "try..finally" exception handlers ? i.e. this proposal can only work when there are are errors raised and the code cannot continue normally and must pass by exceptionb handlers
try...finally isn't about exception handling. it interacts with exception handling, to be sure, but the entire point is that it's code that runs whether or not there's an error, or even if there's a return statement.
You did not need to add this precision because I added it myself in several parts, notably when I refered to the fact that they were ported from C to C++ as a complement of exception handling. But in the Windows API the concept of "try...finally" is called "Structured Exception Handling (SEH)" and not "Structured Error Handling". That's because exception handling in C++ is implemented with SEH, i.e. "try...catch" internally uses "try..finally", in an "hidden way", but the "try..finally" you use in C++ is to add code to the "finally" block (the rest of this block is generated by the initial exception handler, and even if you don't write "try..finally" in C++, there's a "try..finally" block intrinsicly generated for your "try..catch" block, as if you had explicitly added an empty "finally" block.
The exception handler may retry by using a tralining recursive call, but there's no way to return directly at the point where the exception/error was raised, without reconstructing a safe context (by the trailing recursive call: all variables that were in scope when the exception/error was raised are lost, except those passed in the error object constructor; note that to work correctlt this requires that this error object is created first before entering into the "try..."; that object should then contain all the necessary datastructures that may allow the exception/error handler to get significant data and possibly restart the function recursively by a trailing call in a return statement; an exception handler however has no obligation to perform such recursive call, in which case the code will only continue after the "try...finally", whose internal scope is no longer accessible).
I think that "try...finally" (as defined in an extension for C and then ported to C++ in complement of "try...catch") is the way to go with Lua.
While I do like try...finally in terms of expressiveness and robustness (it's way more well-defined than <toclose> in the face of multiple errors) it's also a lot more effort on the programmer's part and doesn't solve the "forgot to release it" issue -- it just makes it easier to make sure it happens. I think <toclose> is a better fit for existing Lua programming paradigms.
try..finally are perfect to handle the "toclose" concept: instead of declaring toclose variables, you just declare local variables within the "try" block. When the block will exit for any reason (explicit return of function, exception/error, or because you reach its "ending" instruction (not marked by an "end" keyword but by the continuation of the code that reached the position just after the last token before the "finally" keyword) all local variables declared in the "try" sub-block get out of scope, are definitely no logner reachable, can have their values dereferenced (and then garbage collected).
Yes this requires discipline: to be able to close a file that has been opened in the "try" block, its file variable must be declared BEFORE entering the "try..finally...end" block (you can also declare your error tracking variables outside of it, it's not necesssary to create/instanciate a new error object within the protected block, as it's enough to pass the values or references of the values you want to give to the error handler to one of these outer variables.
Then the finally block has all it needs to decide (looking at these outer variables that indicate a status or the presence of an error) if:
- it will not handle that error/exception, in which case it can rethrow it (in Lua it can use the "error" Lua keyword for that)
- it will handle the error but will return from the function (not allowing to continue after the end of the "finally...end" subblock) with specific values
- it will return with a recursive trailing call to the function to retry, after having performed recovery/cleanup and possibly modified the values to submit as parameters to the recursive call
- it will modifiy some other status allowing an external code (enclosing the "try...finally...end" block) to adopt another behavior (for exemple set a condition that will break an enclosing loop), and then continue just after the "end"
- it will do nothing and will then let the code continue after the "end".
Typically the "finally...end" would behave in Lua like an anonymous function, called using a trailing recursive call from within the "try...finally" subblock, each time there's an error or exception occuring in it (the "error" can come from the code inside the "try...finally" subblock or from anyone of the functions it called: because this will be like if the code performed a return with a trailing function call, it will behave almost like a jump: no stack used at all (for the case of "error", it firsts calls the generic error handler that will lookup the call stack to unwind it up to the last exception handler, and then will unwind the stack and restore the context that was current at the start of the "try...finally...end" block just before performing the equivalent of a return with a trailing call to the error handler: this will also be a simple jump, so there should be no problem in terms of memory allocation, notably on the stack, because Lua ensures a minimum stack size left at top to be able to perform a call to its internal error/exception handler, that does not need any parameter from the code).
Note that this also requires discipline from the programmer: they must avoid using "error(complex _expression_)" to build a new object with complex function calls or evaluations that could themselves generate errors/exceptions; using the "error" keyword in Lua should be done with minimal parameters, already constructed before, or using simple values like strings, numbers or a reference to a preexisting value).
And with try..finally..end, it's then trivial to write or port the classic "try..catch" exception handlers of C++.
In summary, no need of "toclose" (whose semantic, scope and lifetime is ill-defined). No need of any specific modification to the garbage collector.
But if a programmer is lazy and declares a new varaible inside the "try...finally" 1st subblock, the consequences will be also the same as if it had forgotten to use the "toclose" feature. This is also much easier to maintain! No need to use any specific item in the metatable (in fact we don't even need any metatable, and this works as well on any variables, not jsut varaibles containing reference-type values for tables or for userdata, thread objects...)