Hi all,
In a class method, if you enclose the return within a try/catch block, then if afterwards you call a nonexistent method of a class (this one or any other), the compiler will not call an error, and will simply stop executing code.
I think this situation merits feedback from the compiler or syntax checker.
Minimal demo example below:
/*
save this file as TryReturnTester.sc
then execute:
LanguageConfig.addIncludePath(thisProcess.nowExecutingPath.dirname);
thisProcess.recompile;
then create an instance of the class:
a=TryReturnTester.new
then run the code block below, which behaves as expected
(
a.foo; //no error or syntax issue reported
didfdifhdjif; // ERROR: Variable 'didfdifhdjif' not defined. (as expected)
)
then run the code block below, which doesn't give an error where expected
(
a.foo; //no error or syntax issue reported
a.doesntexit; // no error reported. silently fails and halts further execution
"are we still running?".postln; //doesn't get executed
)
this issue exists if you call a nonexistent method of a different class, as well:
(
a.foo; //no error or syntax issue reported
Array.doesntexit; // no error reported. silently fails and halts further execution
"are we still running?".postln; //doesn't get executed
)
*/
TryReturnTester {
*new {
^super.new.init
}
init {
}
foo {
try {
"foo has been executed".postln;
^true;
}{
"ERROR".error;
};
}
}
Hm⦠the block/break mechanism depends on ^-returning from within a function, so we canāt just disallow returns within functions. (Also there must be hundreds of methods in the class library that use ^ within functions for loop early exits, e.g., detect.)
Maybe the compiler could recognize ^ within try or protect specifically�
Thanks @smoge, I personally will of course never return from within try again, now that Iāve spent hours hunting down what was corrupting my code execution, in a much larger and complicated program.
My hope is that we may be able to spare future users this experience by checking for this at the compiler or syntax level. In my opinion, error handling and reporting is by far SCās biggest area of improvement and this is one example of that.
Recognizing ^ within try and bringing up an error would seem like a great approach to me. Since it breaks things to do it, it would seem that there would be no harm in disallowing it entirely.
So the issue is that SC doesnāt know about ātryā at the compiler/parser level. SC has very few true keywords (words that are known at that level, while is perhaps the only one). Try and other exception based methods are actually no different from other methods, they use the ^ syntax to implement their features. Making the parser/compiler know about these methods means lowering them to that level, this is major language change, both in terms of code changes, but also in design philosophy.
In my opinion, these notes donāt quite capture what Iām pointing out here. They point out that an exception caused by the return function canāt be caught; but the issue Iām pointing out is that even without an exception in the return function, exceptions thereafter, of a particular kind, donāt get called, halting program flow without any feedback. This is a pretty subtle issue to figure out in certain scenarios.
I wish I understood better how the compiler and parser work in SC, so appreciate your insight here.
One thing to note is that my demo code above shows that itās only certain kinds of exceptions that are muted following the return-within-try, specifically calls to nonexistent class methods (there may be other kinds of exception that get muted too). Since basic exceptions still do get called (like my didfdifhdjif; above), maybe thereās something to figure out here about the difference between these two types of exception?
Itās perhaps simpler than you think: as far as the compiler is concerned, there is no such thing as try. Itās just a method call, that happens to do something sneaky which gives us error handling provided that you use it within acceptable bounds. But it is only a method call.
What Jordan means by a change in language design or philosophy is that, once the compiler introduces special compiler cases for specific method selectors and receiver types, then it may be impossible in some cases to override that behavior in the class library.
Also hereās an example where the compiler couldnāt help you:
SomeClass {
fu {
var f = { "bar".postln; ^this };
f.try { ... error handling ... };
}
}
The f.try expression ā the compiler canāt guarantee what is f ā in this case itās trivial, but if f is assigned from branches, or somehow passed in from another method somewhere, the compiler canāt be sure that f will or wonāt contain a ^.
Youāre not at all wrong that itās a terribly confusing trap to fall into, but Iām not sure thereās a 100% ironclad compiler solution.
That all makes good sense, @jamshark70, thanks. Hopefully thereās some way forward here, but I get the difficulty. And in any case, I personally have learned my lesson on this topic
Just an idea: since File.use already exists in SC, why not apply this same model (for acquire-release patterns)? It naturally avoids the āreturn inside try bugā because the return happens at the method level, not inside the block.
For reference, this is File.use from the Standard Library:
Following up on my earlier use suggestion - been in the code lab working on this a bit. Why not something that flips the try/return corruption on its head?
Hereās the move: SafeTry makes corruption structurally impossible. The pattern wraps your code in protect, runs try/catch inside, and keeps returns at the method level
Hi @smoge, thanks much for this! I just tested it with the example code I included in my initial post in this thread, and unfortunately SafeTry exhibits exactly the same behavior as ātryā (I wonāt repeat that behavior here ā itās identical to what I describe happens with ātryā).
If Iām reading smogeās intent correctly, I think heās trying to create a mechanism where method-return within try would feel unidiomatic and users would naturally avoid it. That is, with vanilla SC syntax, ^ within a do loop is harmless but within try is deadly, and it isnāt clear from the syntax why that would be. By creating a syntactic difference, it may be easier to distinguish them and set up āno early returnsā as a best practice.
@smoge one refinement may be to disallow concrete return values from SafeTry. There is at least one bug (with try inside a loop) where the return value is unreliable. My opinion is that constructions like x = try { ... } should be avoided for this reason ā you might get away with it sometimes, but then crash into this bug in another case.
So I spent a while debugging loops inside try blocks, like the max example on GitHub. Turns out, the code returns inside the exception handler correctly, but when it leaves the try method, it then returns back to the loop.
Iām not 100%, but I think this is an issue with jump byte codes, as they increment the instruction pointer, meaning the next instruction in memory isnāt the next logical instruction, as that would be the return. Exactly how to fix this though, I am unsure about. Perhaps the frame needs to store some extra information about the instruction?
Truth. Itās essentially a wrapper that adds features around try/catch, not a fix for the return-inside-try issue. Itās like putting a safety rail around a hole, while adding some debug and handling features, and maybe creating an idiomatic solution.
Anyway, as I mentioned earlier, it does not address any core language issues.
James, I got what you said, and I think that it would make it more useful. By removing the ability to return values, it ālimitsā error handling capabilities, but it would prevent two classes of bugs at once. By design, it would force cleaner separation of error handling and value computation
The parser canāt reliably forbid ^ there, but we can lint the typical case: a block immediately following one of those selectors that contains a nonlocal return.
āCode actionā (āconvert to return-at-method-levelā): turn
try { ^compute() } { |e| ^fallback }
into
var ret;
try { ret = compute() } { |e| ret = fallback };
^ret
NOTE: A treeāsitter would do the same, but safely and precisely. For our returnāinsideāhandler issue, it turns a weak text search into a precise AST ruleāand thatās the difference between āworks in my demoā and āalways works while you type.ā