Special Syntax for FOR-EACH/etc. to receive ACTION!s?

Here is some pretty innocuous-looking code:

n: 1
for-each item block [
    print ["the" item "is" n "in the block"]
    n: n + 1
]

But because Rebol runs ACTION!s when they are bound to WORD!s, and you can put ACTION!s in BLOCK!s, you can get into trouble:

muhaha: func ['x 'y 'z] [print ["stealing your args!" x y z]]
block: compose [10 (:muhaha) 20]

Enumerating such a thing produces total garbage:

the 10 is 1 in the block
stealing your args! is n in the block
the #[void]
the 20 is 3 in the block
== 4

I'm not concerned about malicious cases--Rebol is fundamentally not secure about this kind of thing (see and discuss at "The Philosophy of Security in Rebol") But you want to write code that gives reasonable error messages, especially when writing a mezzanine routine.

Unfortunately, doing this "the right way" is ugly (and it doesn't actually work in a general sense):

n: 1
for-each item block [
    print ["the" :item "is" n "in the block"]
    n: n + 1
]

That only has one reference to the item, but it's clearly much uglier in real cases. But beyond being ugly, tacking a : onto the front of everything doesn't have the same semantics. What if you think you're dealing with a block of objects, and want to call methods on them? item/some-method and :item/some-method aren't the same. (Note: perhaps (:item)/some-method should work?)

Point being: even if you think adding colons is mitigating the problems, you're not getting what you really want...which most of the time, is an error.

Could we make the common case work better?

Something I've often wondered is if these enumerators which take words and set them through a loop would only let you get at ACTION!s if you used some other ANY-WORD! type. This way, they could error otherwise...and common loops could feel safe.

For the sake of example, let's say it's a GET-WORD! passed to FOR-EACH if you actually are prepared to work with ACTION!s. The first example above, by using a plain WORD!, would output a clear error instead of gibberish:

the 10 is 1 in the block
** Error: Variable `item` must be GET-WORD! to hold ACTION! in FOR-EACH

If GET-WORD! were used, it would make some amount of sense--and line up with the fact that you would be wanting to use GET-WORD! in the body too. But it sacrifices the current feature for GET-WORD!, which is "soft quoting"...where the word specifies the word to look at to find the word to use:

 >> word: 'item
 >> for-each :word [1 2 3] [print [item]]
 1
 2
 3

However, there's another way of doing this, with GROUP!:

 >> word: 'item
 >> for-each (word) [1 2 3] [print [item]]
 1
 2
 3

Soft-quoting doesn't really come up all that terribly often. Still, it's a little annoying that this would throw a wrench into COMPOSE situations doing simple soft-quotes, but you could attack that multiple ways.

I think it would be much better if you could mark very clearly which enumerations were intentionally working with ACTION!s. The error messages would be better, and people are savvy enough to know there could be a problem won't be so paranoid in their basic enumerations--knowing the error will be delivered.

Should just loops be affected, or all soft quotes?

It could be loops for starters. They could be switched to hard quotes and do their own logic, only giving soft-quote semantics for GROUP!s.

Or maybe soft-quoting is too sacred as a mechanism in PATH! processing...and you don't want to have to type foo/(bar) instead of foo/:bar...so that translates to wanting to keep it in sync.

This might mean using another datatype, e.g. for-each @item block [...] to say "ACTION!s are okay".

Any thoughts? I know Rebol has some elements of "it's a fundamentally unsafe language", but I just feel there need to be some limits. But I don't want to bulletproof every FOR-EACH in the system against function injections--even if they are all just accidents, you want better feedback than having a mess be made.

3 posts were merged into an existing topic: GET-WORD! for loop vars / function args mean "Allow ACTION!s"?

This has been on my mind a long time -- this thread began in Dec '18. (I've archived the failed attempts at solutions)

I'm one of those people who tries to write generic code and frets about what would happen on the day that unset variables or ACTION!s come along. So I'm caught between the balance of feeling negligent by not peppering with checks, or junking up otherwise elegant code for the sake of something that could happen.

And It's Finally Solved! :trophy:

It's solved through lifted variables.

Lifted variables are those which store meta-representations of what they are assigned. If you do a lifted assignment, and a lifted retrieval, you can round-trip anything:

>> ^anything: 1020
== 1020

>> anything
== '1020

>> ^anything
== 1020

>> ^anything: ~
== ~  ; antiform (trash)

>> anything
== ~

>> ^anything
== ~  ; antiform (trash)

Given this property, it makes them an ideal insulator against the active dispatch of an action.

>> ^anything: append/
== ~#[frame! [series value :part :dup :line]]~  ; anti

>> anything
== ~#[frame! [series value :part :dup :line]]~

>> ^anything
== ~#[frame! [series value :part :dup :line]]~  ; anti

So quite simply, a FOR-EACH variable which is not lifted won't accept actions.

>> obj: make object! [field: does [print "I'm an action"]]

>> for-each [key val] obj [probe val]
** Error: Can't assign VAL antiform ACTION! in FOR-EACH, use ^VAL

>> obj: make object! [field: does [print "I'm an action"]]

>> for-each [key ^val] obj [probe ^val]
~#[frame! []]~  ; anti

Lifting lets you receive trash as well. And what's great here is that if you leave off the lift, it will work fine so long as you don't hit anything that needs to be lifted. Your default undecorated code is "safe". It's only when you enumerate actions and trash that you get notified that lifting is needed.

Quite the perfect solution. Case closed. :slight_smile:

2 Likes