QUIT Rethink: Antiform Errors, Being Definitional, Etc.

Let's say you have script like this:

Rebol [
    File: %quitter.r
]

print "I'm a quitter!"
quit
print "This doesn't run"

In Rebol2 if you run it from a prompt with do %quitter, it will print I'm a quitter and immediately exit the interpreter. Red does the same thing. (though you have to change the header from "Rebol []" to "Red []")

But in R3-Alpha, you get:

>> do %quitter.r
Script: "Untitled" Version: none Date: none
I'm a quitter
== 0

There's nuances about QUIT in R3-Alpha as well, that you can see in the HELP:

USAGE:
    QUIT /return value /now

DESCRIPTION:
    Stops evaluation and exits the interpreter.
    QUIT is a native value.

REFINEMENTS:
    /return -- Returns a value (to prior script or command shell)
            value -- Note: use integers for command shell
    /now -- Quit immediately

While Red and Rebol2's /RETURN only took INTEGER!, R3-Alpha could take any other value...which can be returned to the calling script.

It also offers a /NOW, in case you really did mean you wanted to exit the system entirely.

"/NOW" Not Being the Default Makes Sense

"Quitting" a script shouldn't casually force exit from the entire interpreter when you DO it. So I agree with R3-Alpha's choice on that.

But I think I prefer moving the /NOW functionality into a different routine, to convey just how different that is. sys.util/exit seems like a good way to say it. That's a function that breaks the boundaries of what a more polite routine like QUIT can do.

QUIT Should Be Able To Return Arbitrary Values

I think that allowing arbitrary values to be returned to the caller via DO instead of just integers makes a lot of sense. Rebol2 and Red both let a script return that arbitrary value if it just fell out the bottom of the script... so why shouldn't you be able to do that with a QUIT parameter?

(Though actually, I'm not sure that arbitrary values should just fall out the bottom of a DO script. This would seem to leak values in a lot of cases that weren't intended... showing information in the console which is irrelevant (at best) or a "security leak" (at worst). The default should likely be nothing (~ antiform)... which is interpreted as the script succeeding by default (0 exit code). If you want the script to synthesize a value, you need to explicitly QUIT with that value...same as with RETURN in FUNC)

But arbitrary values gives us the question of conflating arbitrary integer return results with exit codes. You might have a script which wants to return an integer as a count of something, and any non-zero counts could be interpreted as the script having failed. But if you call this script from the command line, it shouldn't be treated as if it failed.

It seems clear to me that this is a job for definitional errors! QUIT needs to differentiate between returning values "as is" (all of which should represent success if called from the console) or returning raised errors, with the exit code embedded. This would allow a DO of a script that raised an error to correctly fail by default, if you didn't handle the error with an EXCEPT.

What Interface Should QUIT Use?

We need a way to say quit with 1 as raised error vs. quit with 1 as ordinary value. I think that the former is probably the more typical meaning. And I'll mention that I don't like /RETURN as the name of any refinement that isn't dealing with function RETURN.

It's probably best it's made to be arity-1. quit 0 isn't that much more to type than just quit, and reinforces that you have a choice there.

If you said QUIT 0 that would translate into just returning nothing, so you'd get nothing displayed if you called the script from a DO. But you'd get a 0 exit status from calling it at the command line.

By default you could only pass it integers, but some refinement would switch it over to meaning you want to quit with a value. Probably something like quit/value 1 would do the trick. That would give back the literal value 1 to a DO that called it, and look like success with exit status 0 if you called it at the command line. (Maybe the option to print the result should be offered as a command-line switch, so command-line programs have at least some way to get a non-integer value out of a script that catered to DO from other scripts?)

Big Thought: Definitional QUIT

Right now catching QUIT is a bit of a black art.

It seems to me that the way this should work is that each QUIT is definitional, throwing back to the DO or IMPORT that rigged up the module structure and ran it.

When you're running code from the console, that too should have its own QUIT...that just quits the console. That way, when it gets caught the console can know it caught just the one that was intended for it, and can tidy up. Not only that, the interface to that QUIT can offer an arity-0 form without affecting the rest of the system. (Slightly confusing perhaps, but people will figure it out.)

There are some strange consequences of this. It means that every time you run an evaluation in the console, you're dealing with a different QUIT--it's a QUIT that is specifically scoped to that console evaluation. It can always be found in the same place (e.g. the QUIT variable of the user context)...but it will be refreshed each time there is a new evaluation to be caught.

Further, it means modules can't QUIT after their initial execution. This actually makes a lot of sense... who would process that module's QUIT when it isn't running on the stack? You can only quit a module while you're initializing it...after that point, your only option is to SYS.UTIL/EXIT and terminate the whole interpreter...because there's nowhere else to meaningfully tie a quit to! Good stuff.

This All Looks Quite Good

Seems to fix a lot of suboptimal things related to quitting we've had going on.

The code that tried to guess whether non-integer values meant success or not was a very early example I encountered of "hmmm...this doesn't seem right" :thinking: ... it will be nice to rip that out and operate on the basis of isotopic rigor! :atom_symbol:

Definitional quit has a consequence that all definitional features do: you can't call a subroutine that engages your notion of QUIT unless you pass it your QUIT.

Hence it isn't possible to call a library function QUIT-IF-TUESDAY and have it psychically know what your meaning of QUIT is. Your subroutine would have to be phrased more like like if-tuesday [quit].

It's not entirely a bad thing that modules other than your own can't casually quit on your behalf. I think it likely leads to better design. But it will be something that confuses people.

The sys.util/exit function is something people they reach for, if they really want to force the interpreter to exit from any point. Because it would be used more casually in non-crisis situations, it should probably be re-engineered to do graceful shutdown instead of just calling exit(). (unless you say something like sys.util/exit:abrupt)

We could make this a bit less antagonistic for the console quit case, by saying that the "real quit" is something called QUIT*. Then QUIT would call that ever-changing value of QUIT*... while retaining the identity as a function you could wrap up with specializations and adjustments.

But again...those changes you made would only apply to the console's QUIT. It wouldn't affect the QUITs of modules or scripts.

If you wanted to write a wrapper like MY-QUIT which could be used in places that had various different definitions of QUIT, you'd have to pass the quit you're referencing to it, e.g. my-quit :quit [...] at every callsite. We now have the notion of looking up things in blocks, so if you did take a bound block at the callsite you might be able to extract its quit definition from there... my-quit [...]

As for a greater macro-facility to be passed the environment at the callsite to do the lookup...well... that's an idea. But if you made something named QUIT then it would only find itself, unless there was some way to "peek underneath" in the environment to find out what QUIT used to be. :face_with_spiral_eyes:

Let's put a pin in all of that. I've implemented a first cut at the definitional QUIT and it was very pleasant to rip out all the belabored stuff that was there before... and I don't think anyone's going to miss it.