Open rebolbot opened 14 years ago
Submitted by: BrianH
ATTEMPT in R2 was incorrectly catching RETURN and EXIT because the mezzannine was missing a [throw] attribute. Fixed in 2.7.7:
>> attempt [exit]
** Throw Error: Return or exit not in function
** Where: attempt
** Near: exit
As for whether ATTEMPT should be catching that error, this points to an interesting detail about how the BREAK, CONTINUE, THROW, QUIT, HALT, RETURN and EXIT functions (all basically gotos) are implemented and where, exactly, the error is generated from.
The goto functions all throw to a handler, different for each of those functions, if one has been established. If no handler has been established then the fallback handler triggers the error you see. The goto functions don't actually trigger those errors at all: They don't know whether they will be handled, they just throw their respective stuff.
Now the tricky part is that ATTEMPT [EXIT] is not necessarily erroneous code, and as far as the ATTEMPT is concerned it isn't erroneous at all. The error you see is generated by the top-level code, outside of the ATTEMPT, not by the EXIT function itself. So what you are requesting is for ATTEMPT or TRY to handle an error before it is triggered or even known to be erroneous.
The only way to do that would be to trigger the error earlier, but that can have some problems. For one thing, triggering the error outside of the handler would depend on being able to tell from the outside whether the throw is being handled by the fallback handler or not, which might not even be possible (Carl?), and would certainly involve task-local management overhead. That overhead would need to be added to every call to EXIT, RETURN, THROW, BREAK and CONTINUE if those functions were modified to trigger an error.
The worse alternative would be for ATTEMPT and TRY to catch all throws from those functions, check the handlers, then rethrow if they are OK. If that could even work it would add huge overhead to all of the unstructured return code, so it's a bad idea.
Unless Carl can come up with a magic method to make all of the above problems go away, I'd have to recommend that this ticket be dismissed. Not as "not a bug", but as a subtlety in the nature of the error that you just need to consider.
Changed the summary to reflect what is going on here.
Submitted by: Sunanda
Thanks for the detailed analysis, Brian. But I am not sure we can yet agree on the issue involved.
I take a simplistic view: the code below should always print "still running" no matter what 'f is....
attempt [f]
print "still running"
....The only exceptions being if 'f executes a console-stopping word, eg:
f: func [] [halt]
f: func [] [quit]
f: func [] [q]
I fully accept that I need to defuse such console-stopping words before being able to execute arbitrary REBOL code and expect the console to still be running afterwards.
But there are other possible 'f that crash the console. The two Curecode reports cover two of them:
f: func [][return break]
f: func [][return exit]
Key questions are:
1. Are there more of them?
2. How could I get the complete list?
3. Will there be more of them in the future? (R3 has added the EXIT problem; R2 could handle that)
4. How can I protect against BREAK or EXIT being misused in arbitrary code while allowing them to be properly used in the same code?
The current behavior allows me to easily craft attacks against arbitrary code. And so forces me to use CALL to launch another instance before executing arbitrary code.
Surely it would be better to (say) have a SECURE policy to set ATTEMPT and TRY as STRICT.
Submitted by: BrianH
Sorry, there are more functions than just HALT and QUIT that are supposed to stop execution. The THROW, BREAK, CONTINUE, EXIT and RETURN functions also stop execution, though where execution resumes is bounded by a different scope.
Now ignoring the already reported issues (in high priority tickets, no less), and unanswerable questions 1, 2 and 3, we are left with question 4, and the answer is simple: There is no way to tell the difference without running or otherwise tracing the code. For instance:
>> a: does [break]
>> loop 2 [print 1 a print 2]
1
>> loop 2 [print 1 attempt [a] print 2]
1
>> a: does [attempt [break]]
>> loop 2 [print 1 a print 2]
1
>> loop 2 [print 1 attempt [a] print 2]
1
>> a
** Throw error: no loop to break
This is all correct code. There are no errors here. ATTEMPT [BREAK] is never itself an error, it can only be an error in context (real context, not a binding thing). ATTEMPT, TRY, EXIT, BREAK, RETURN, and so on are all functions, not syntax. A BREAK may be called from any number of nested function calls, and it doesn't matter that these look like syntax, they are still nested function calls. And ATTEMPT and TRY should never catch HALT, QUIT, EXIT, RETURN, BREAK or THROW, because doing that would be an error in almost all code. Those functions are innocent: It's their handlers that are at fault, not them.
So the question needs to be whether EXIT, RETURN, BREAK, CONTINUE or THROW should be provided with the information at runtime about whether they would be handled by the error-triggering handler, and if so then trigger the error themselves instead of doing their job (HALT and QUIT should never be caught by ATTEMPT or TRY under any circumstances). It is likely that this would add unacceptable overhead to the process of calling and returning from functions, loops, CATCH and DO (Carl?). If that is the case then we should give up on this.
"R3 has added the EXIT problem; R2 could handle that" That was an error in ATTEMPT, which was catching all RETURN and EXIT calls, especially the legitimate ones. Fixed in 2.7.7.
Submitted by: BrianH
Another consideration is that what BREAK, CONTINUE, EXIT, RETURN, THROW, QUIT and HALT do already is trigger a kind of error (code: 0 for BREAK), and that the error message that you see above actually comes from that error already; see system/catalog/errors/throw for the relevant messages. These errors are specifically supposed to be ignored by TRY and ATTEMPT because they don't represent erroneous conditions, they represent legitimate behavior by definition.
So the proposal is not to have the functions trigger errors, it is to have them trigger different errors, in a different category with codes greater than 100, and probably full error objects inside them. And to do this when they can determine that they are not going to be handled by an official handler, but by the main handler instead. Information that they currently can't know.
Submitted by: BrianH
See #1509 for the error? try [break] bug.
Submitted by: abolka
> EXIT, RETURN, BREAK, CONTINUE, [and] THROW should be provided with the information at
> runtime about whether they would be handled by the error-triggering handler, and if so
> then trigger the error themselves
I think that improved error causation as described by Brian in above comment would be worthwhile for the sake of language consistency and should therefore be pursued.
After discussing this a bit on AltME, it seems that a fairly straightforward implementation adding only minimal overhead could be possible. But that's, of course, a question only Carl can answer definitively.
Submitted by: BrianH
Based on the stuff said in http://www.rebol.com/r3/notes/errors.html it seems that BREAK, CONTINUE, EXIT, RETURN and THROW (and maybe QUIT and HALT) don't actually throw their pseudo-error values, they just return them and DO handles the propagation. This means that the solution to this would be simpler: The functions can just create and trigger real errors with the same error codes when the unwinds wouldn't be handled by their proper handlers, rather than returning and unwinding.
Submitted by: Ladislav
"...it seems that BREAK, CONTINUE, EXIT, RETURN and THROW (and maybe QUIT and HALT) don't actually throw their pseudo-error values, they just return them..." - this terminology is unreadable as noted at #1491, at least IMO (Ceterum censeo, Carthaginem esse delendam).
To single out what I see as terminologically insane: "THROW does not throw"
In my preferred terminology, THROW is a function that throws, as opposed to other functions, e.g. the DO function, that does something else, which does not deserve to be described using the word "throw" in my opinion, not even in cases like:
do make error! "phew!"
Submitted by: BrianH
Yes, it is ironic that THROW doesn't really throw in R3, while DO error! does throw, but not with THROW. When you get into language internals there tends to be a lot of ironic situations like this. The naming of the functions is based on their surface behavior though, not their internal implementation model. And the surface behavior of THROW is to be the counterpart of CATCH.
Submitted by: Ladislav
As far as I am concerned, I do not want to explain people, that "THROW does not throw". That is why I proposed to use a different terminology as mentioned in #1491
Submitted by: Ladislav
Rebol [
Purpose: {
A dynamic B-CATCH/B-THROW pair implemented to illustrate how
a dynamic THROW-like construct can be implemented so
that unhandled throws are detectable during the B-THROW call
}
]
make object! [
; indicating B-CATCH presence:
b-catch?: false
set 'b-catch func [
block [block!]
/local result previous
] [
; remember the current state
previous: b-catch?
b-catch?: true
set/any 'result catch block
b-catch?: previous
:result
]
set 'b-throw func [value [any-type!]] [
unless b-catch? [do make error! "unhandled B-THROW"]
throw get/any 'value
]
]
Submitted by: Ladislav
Definitional Return note: since the test of function context availability has already been implemented in R3, it can also be used to test whether a definitional Return will be caught.
The desire Sunanda expresses here is reasonable--and it has lately occurred to me that there should be a construct which "cannot be passed". It could intercept raised errors (e.g. by "fail", which are not passed up through the stack--they use longjmp and can happen abruptly)... or if a cooperative "throw" tried to pass it, that throw would be converted into an error, and it would intercept it in the form of a "no catch for throw".
I've wondered if this might be the distinction between a TRAP and a TRY. Ren-C had been using TRAP as a more consistent-sounding name for TRY (as TRAP/WITH pairs with CATCH/WITH better, and TRY/EXCEPT feels a bit awkward in the Rebolverse).
This is of particular applicability in the API, where most code doesn't want to allow things like a RETURN to be able to "jump past it".
Submitted by: Sunanda
BrianH has dismissed, with an explanation, a similar bug:
CC#583: neither R2 and R3 cannot recover from executing this bad code, even though it is wrapped in an ATTEMPT or TRY:
This is the new bug: R2 and R3 differ in their behavior with TRY [EXIT]; and R3 has the least useful behavior:
R2: traps the error nicely:
R3: fails similar to BREAK:
CC - Data [ Version: alpha 97 Type: Bug Platform: All Category: Error Handling Reproduce: Always Fixed-in:none ]