Permissive Group Invisibility

While messing around with some experimental debugger code, I made BREAKPOINT an invisible that doesn't take any parameters. This means you can write foo + breakpoint bar. It would break after foo evaluates as the first argument to +, before bar evaluates as the second arg to +.

But should you be able to say foo + (breakpoint) bar ?

The idea that a GROUP! could simply vaporize away if its contents are empty or invisible seems a bit unsettling. So these cases were errors:

>> 1 + () 2
** Error: + is missing its value2 argument

>> 1 + (comment "Hi") 2
** Error: + is missing its value2 argument

But how unsettling would it really be if those worked?

>> 1 + () 2
== 3

>> 1 + (comment "Hi") 2
== 3

I had said this was too weird to be justified "just for some comments". But when you think about a interesting multi-parameter breakpoint, wouldn't it be nice to put it where you want it and group it as well?

foo + (my-complex-breakpoint #arg1 #arg2) bar

You wouldn't be able to do this if grouping forced the production of a ~none~ isotope out of that emptiness. The same would be true for using DUMP today:

foo + (-- x y z) bar 

As invisibles become more an integral part of the system, it no longer seems like being weird for the sake of being weird.

Permissive Invisible Groups

An argument for groups not vaporizing is if you think of (...) as being a synonym for do [...]. If your code is coming from an unknown source, and it's a GROUP! may not know if it's going to vaporize or not. But...

  • that the actual scenario for GROUP!s? How often are unknown code bits being passed around as GROUP!, and then executed by being spliced into evaluation? Aren't blocks the currency for that?

  • why not just use DO on your groups if you're worried? If you do not know what kind of code is in a GROUP!, it must be coming by way of a variable. So the way you are getting the evaluator to run it is either DO (where this isn't an issue, because empty groups return void) or you've made an effort to splice it into a block so it evaluates inline. If you're forced to splice, why not splice compose [... do '(my-group) ...] if you don't want the ability to vaporize? It's a short word, and it liberates GROUP! to a distinct novel usage and behavior.

Of course, it's a thing you'd have to learn about GROUP!... that they aren't a synonym for DO of a BLOCK!. But it's a nuance... groups just group things, they don't synthesize any VOID!s or artificial values of their own.

The nuance is pleasing, and simple. I found that I had a preference for:

 case [
     something [...]
     (elide print "Got this far")
     something-else [...]
 ] opposed to:

 case [
     something [...]
     elide (print "Got this far")
     something-else [...]

(Though technically you don't really need a GROUP! at all in that case, but the use of groups is often a subjective thing.)

And if there isn't permissive group invisibility you won't get it any other way. I feel like there's multiple points in favor of this... expressivity, and a coherent story: "groups only function is to group things, they are ghostly, () 1 + () (comment "hi") 2 () is 3"

(Note: Because the GROUP!s are not enfix functions themselves, they are not entirely ghostly...they are "only as invisible as non-enfix invisibles are". This is to say that 1 () + 2 can't be the same as 1 + 2, because, the 1 doesn't see the + through the group!. It can't start evaluating it either to find out if it's empty and then have to back out of this is an error.)

(Also worth pointing out: if you COMPOSE a GROUP! which has null or splice an empty block via group...not even -invisible-, you get it vaporizing: compose [1 + (if false [<not-spliced>]) 2] => [1 + 2])

1 Like

I had an insight for why in Ren-C's world this is a particularly important design decision.

First the setup...

Historical R3-Alpha/Rebol2/Red had problems with code of the form:

r3-alpha>> parse "abcde" rule: ["a" "b" (clear rule) "c" "d" "e"]
== true

This may be dismissed as an edge case and "why would you do that" and "if you do that you get what you deserve". But the only reason it hasn't crashed is you are using stale past-end-marker data in a series that hasn't been GC'd. And such a situation can be much worse than crashing...not crashing can be silent and go haywire causing arbitrary damage to your data, when those out-of-bounds accesses somehow represent new instructions to the system's internal implementation.

Ren-C locks series against mutability while they are being enumerated--including PARSE rules as they run:

ren-c>> parse "abcde" rule: ["a" "b" (clear rule) "c" "d" "e"]
** Access Error: series has temporary read-only hold for iteration
** Where: clear subparse parse main
** Near: [clear rule ~~]
** Line: 1

This may sound harsh, but in PARSE's case you have other options. You can add material to a rule while it's running. Use a subrule that is not currently itself executing

ren-c>> did parse "abcde" rule: ["a" "b" (append last rule ["c" "d" "e"]) []]
== #[true]

And you can remove material by a similar token, if you put it in a sub-block to start with:

ren-c>> did parse "ab" rule: ["a" "b" (clear last rule) ["c" "d" "e"]]
== #[true]

Now the punchline...

Permissive group invisibility gives DO the same power to defeat the mutability lock (where adding is concerned). Because now you have a way to preserve the result if you don't add anything to the nested group:

ren-c>> do code: [1 + 2 append last code "hi" 3 + 4 ()]
== "hi"

ren-c>> do code: [1 + 2 3 + 4 ()]
== 7

That's just a simple example, but I'll re-summarize by saying that R3-Alpha had problems with generic mutability (bad invariants at best, potentially arbitrary hacker-level bugs corrupting the evaluator at worst...though in DO's case it used to check the tail index so that particular enumeration was bad-invariants-only). And solutions like this--if you thought to use them--would not preserve the last result:

r3-alpha>> do code: [1 + 2 3 + 4 ()]
; prints nothing (e.g. UNSET!), as opposed to 7

Just interesting to see another argument for why the invisible GROUP! being invisible has leverage, making () as "UNSET! generator" is inferior.

This cannot be changed in Redbol (unless some kind of deeper evaluator hook is introduced).

1 Like