Pushing the Potential of Polyglot Polymorphic DO

The brevity of DO is a strength that makes it seem natural to be polymorphic.

I thought it was very wasteful that when Rebol2 and Red's DO saw something it didn't recognize, it would just drop out:

rebol2>> do 1020
== 1020

rebol2>> do <useless>
== <useless>

Pinning people's expections to this behavior--to where they write code that relies on it--is awful.

There are plenty of things we can imagine for this undiscovered space. (Soon it's planned that the ReplPad will use shorthands like do 3 to mean "run the code in editor tab #3")

Could We (or Should We?) Take DO's Polymorphism Further?

I'd been thinking that it might be able to "run" things that are not even Rebol. This would mean that you could do %foo.js and run JavaScript, or do %bar.css and import CSS.

(But then again, JavaScript has modules vs. ordinary code... so maybe import %foo-module.js would also be an interesting polymoprhism to have as well?)

We are getting some experience with this and the ReplPad. And what experience is telling us is that it's really ugly to have to use names like JS-DO and CSS-DO. Plus it puts stress on coming up with and remembering the names (was it JS-DO or DO-JS?)

With FRAME!s we even have the option of building things that defer execution. Some simple pseudocode using current JS-DO to give the idea.

load: enclose :lib.load func [frame]
    uparse try match [url! file!] frame.source then [
        let result: make frame! :js-do
        result.source: frame.source
        return result
   ]
   do frame
]

>> thing: load https://example.com/helloworld.js  ; doesn't run JS yet

>> do thing  ; would run it here

In today's Ren-C, the sky is the limit for many such things.

I Think DO of a TEXT! String Should Be Dropped (Reclaimed)

When you DO a BLOCK!, you know that block has been incarnated through some series of steps that bound it and brought it to life.

When you DO a TEXT! string, you have nothing to go on but the string itself. It represents an incomplete thought, and it's hard to think of a "good" answer for what the semantics of that should be.

One tricky issue we've talked about is how module headers in text strings are handled:

do "Rebol [Type: module] export thing: {This does not work}"

Historically what happens is that Rebol is a synonym for the SYSTEM object, so that evaluates an inert OBJECT! as a first step. Then it evaluates the inert block [Type: module]. You don't have any of the LOADer mechanics in there.

So basically, the above is completely broken.

We're facing another problem of being short on strings to say where code should come from. It's important to have a way to distinguish running paths relative to system.script.path as opposed to WHAT-DIR, and strings seem a reasonable way to encode that intent:

do %../path/relative/to/what-dir
do "../path/relative/to/system.script.path"

import %../path/relative/to/what-dir
import "../path/relative/to/system.script.path"

When you add all this in with the spirit of language agnosticism, that makes interpreting TEXT! as being specifically Rebol language text is presumptuous.

This all makes going through do load to use text as source seem like a much more attractive option than trying to figure out how to push all of LOAD's options onto DO.

>> do "print {Hello World}"
Hello World

>> do load "print {Is DO LOAD that much worse for Hello World?}"
Is DO LOAD that much worse for Hello World?

Of course, I will make the usual point that you will be able to overload this if you wish. Redbol will still DO strings, and you can decide you're never going to use the script-relative path importing (or do it another way).

But I think DO LOAD is a small price to pay for solving a bunch of irritating problems.

I Think We Should Drop the /NEXT Option From DO

Clearly a DO/NEXT of a JavaScript file doesn't make sense. But does it make sense for a Rebol file, really?

Rebol2 returns a BLOCK! with a pair of the result and remaining code, which works for blocks:

rebol2>> foo: [print "Some" print "Block"]

rebol2>> do/next foo
Some
== [unset [print "Block"]]

But for functions, it just ignores the /NEXT:

rebol2>> foo: func [] [print "Some" print "Function"]

rebol2>> do/next :foo
Some
Function

Red gives nonsense, as usual...it returns the value of the function back and doesn't DO it at all

red>> foo: func [] [print "The usual" print "nonsense"]

red>> foo
The usual
nonsense

red>> do/next :foo 'pos
== func [][print "The usual" print "nonsense"]

red>> pos
*** Script Error: pos has no value

So I'm proposing the /NEXT functionality be solely in EVAL, and have EVAL run only on ANY-ARRAY!

If you DO, that means fire-and-forget.

Narrowing DO Use Is Good For Security / Avoiding Big Mistakes

DO is pretty powerful. When you say DO VALUE that could be a URL!...fetching arbitrary code off the internet and running it.

Of course, DO of a BLOCK! can contain code that does arbitrary things. But if you're writing code that's supposed to all run on your machine and be self contained, it would be nice if you could be reasonably sure that you aren't running code off the internet if you didn't use DO or IMPORT.

So making it possible to get normal casual work done locally without ever needing to call DO seems desirable. That is why EVAL has both "do to end" and "do step" semantics:

 >> eval [1 + 1 print "Modes"]
 Modes

 >> [value pos]: eval [1 + 1 print "Modes"]
 == 2

>> pos
== [print "Modes"]

So you can use EVAL where you would have used DO of a BLOCK!, and you can use the /NEXT mode as a multi-return (or a refinement, if you choose)

>> eval/next [1 + 1 print "Modes"] 'pos
== 2

>> pos
== [print "Modes"]

This raises into question if DO of a BLOCK! needs to be a way to run code at all. It could be dialected, and let you supply arguments:

do [%script-taking-args.reb 1 2 3]
2 Likes

Delving further in the semantics of DO...there is a question about whether DO should catch QUIT signals or not...

  • does a QUIT originating from inside the code you pass DO leave the interpreter running, passing back a value to you?

  • or does a QUIT from inside the code you pass DO actually quit the interpreter, passing back an exit code to the shell

As is oft the case, the answer to this question historically is both ugly and inconsistent. :slight_smile: It is controlled by the badly-named /ONLY refinement of DO:

>> do "quit 10"
== 10

>> do/only "quit 10"  ; exits
/home/r3$ echo $?  # here's how to print the status code
10

But regardless of whether you use /ONLY or not, a DO of a BLOCK! won't be able to catch the quit. It always terminates the interpreter.

I Think DO Should Work Like A Function, Only Return Value if QUIT

One thing I see here of /ONLY being an epicycle of is a semantic problem...of not knowing whether a script meant to return a value or not.

When you're running code on the command line with --do, it needs to know whether to return an exit code representing an error or not. Conservatively it doesn't want to just assume because the last line of the code you gave it gave back an INTEGER! that was an exit code. So there's a reliance on the idea of a transparency...if something wanted to quit with a value it would have returned it.

This sorts out a lot when you think of the idea that DO only ever returns a value when there's an explicit QUIT with a parameter passed to it.

It gets very hard in the internals to try and tell you whether a script ran a code path that had QUIT on it or not. So the best measurement of intent would be to say that all DOs are none unless you QUIT with a value, and QUITs are always caught.

(I know @gchiu will like the less-noisy return result, so that a DO is quiet in the console unless it has a reason to be otherwise.)

I'm not sure if it needs to or not. But given that we have EVALUATE, I don't think it needs to be able to catch quits. This means we can universally say that if you run DO, it will catch any QUITs that occur.

So moving on these things slowly, which is tidying up some loose ends.

2 Likes