HIJACK-protection: Preserve a Weird Feature? 🫤

The HIJACK functionality was proposed by @gchiu, many years ago:

>> /foo: func [x] [print ["FOO" (x + 1)]
>> /foo-reference: foo/

>> foo 20
FOO 21

>> foo-reference 20
FOO 21

>> /bar: func [x] [print ["HIJACKED!" (x + 1000)]

>> hijack foo/ bar/

>> foo 20
HIJACKED! 1020

>> foo-reference 20
HIJACKED! 1020

And so, way back when... I worked up a mechanic for how to do this. There's some nuance to how efficient it can be, based on whether the thing you're hijacking uses a parameter list that's in the same "derivation chain".

While it was originally a somewhat fringe feature, it became central to certain parts of the implementation--including the web ReplPad.

What If You Want To Use The Old Implementation?

Note that all references to the old function will run the hijacker. What if you wanted the old implementation?

The seemingly simplest answer would be to return the old implementation under a new identity by HIJACK:

>> /foo: func [x] [print ["FOO" (x + 1)]
>> /bar: func [x] [print ["HIJACKED!" (x + 1000)]

>> /foo-old: hijack foo/ bar/

>> foo 20
HIJACKED! 1020

>> foo-old 20
FOO 21

But I Did Something Different...

In that old era of ACTION! and FRAME! as distinct types, I decided to be clever.

I said that COPY of an ACTION! would create a new action identity that ran the same code... but that wouldn't be affected by a HIJACK of the old action.

>> /foo: func [x] [print ["FOO" (x + 1)]
>> /bar: func [x] [print ["HIJACKED!" (x + 1000)]

>> /foo-copy: copy foo/ 

>> hijack foo/ bar/

>> foo 20
HIJACKED! 1020

>> foo-copy 20
FOO 21

So HIJACK didn't return anything.

On the one hand: this is strictly more powerful, it means any code anywhere can make a new identity and ensure it won't be affected by subsequent HIJACKs of the original function.

It also lets you avoid ordering problems:

 hijack foo/ (adapt copy foo/ [print "Doesn't need an extra step!"])

You'd have to do this in multiple steps otherwise, with some kind of dummy hijacking:

 /old-foo: hijack foo/ noop/
 hijack foo/ (adapt old-foo/ [print "Without COPY, you have to do this"])

So it seemed superior.

BUT this turned out to be more complicated to implement, and opens a bit of a can of worms about the meaning of COPY.

What Should (Could?) "COPY an ACTION" Mean?

Let's just simplify matters a little and think about the actions which have an implementation "BLOCK!" behind them...what I've called the "Details Array".

You might imagine that making a copy that would not be subject to the same HIJACK-ings is as easy as making a copy of that array. Maybe (?) that sounds a little expensive, but, you'd imagine this isn't done too often.

However, consider something like this:

>> /g: generator [yield 1 yield 2 yield 3]

>> g
== 1

>> /g-copy: copy g/

>> g
== 2

>> g-copy
== ???  ; what do you think?

There's actually a huge problem here, in that the Details Array contains delicate state. You can't just assume duplicating that state is going to lead to a situation that won't be confused or crashy. It may contain unique pointers that one of the instances assumes it can free because it thinks it is unique.

Given this reality, the interaction between COPY of ACTION! and HIJACK was very crafty. The HIJACK only did a minor disruption to the original Details array, basically rewriting a bit of it to say "you've been hijacked" but leaving the contents of the array state in place. Copies were small stubs that could chain through to the original Details identity--and despite the fact that it had been hijacked, still run it.

But things got fairly twisted. This meant HIJACK couldn't be simple, and function copies became strange beasts that had to be conscious of the possibility that they were representations of hijackings and be conditional and that.

Further... COPY of "action" is Now COPY of FRAME!

The unification of FRAME! and action brought about a bit of a semantics problem.

COPY of an "action" now is just a mechanism of getting another FRAME! with the same parameters, that you can tweak. It doesn't imply anything about "protect against hijacking".

Hence this notion of "make new action identity that can't be hijacked" would have to be some new operator, not COPY.

We're thus talking about something called make-unhijackable-reference. :face_with_diagonal_mouth:

Or... Just Say "Screw It", HIJACK Returns New Identity?

This is almost certainly the best answer.

Redoing this doesn't necessarily rule out the idea of inventing MAKE-UNHIJACKABLE-REFERENCE some day. But that would mean a hijacking would have to preserve the old implementation in a more "costly" way than it has historically, and those references would also be more costly.

Seeing it relatively clearly after having written this post, I think the added cost would be the right way to do it, if this feature were decided to actually matter to anyone.

So far the only uses of COPY of action to avoid hijackability have been done at the moment of hijacking, to re-use the implementation as part of the hijacking. The concept of "shielding references from HIJACK" for any other reason is not something that I can think of applications for.

(If you are the one exporting a function, and you think you someone might hijack it and you don't want to be subject to those hijackings, you can export an ADAPT with an empty block...or something of that sort...and then your implementation is safe, because if someone hijacks that adaptation it won't affect what the adaptation called.)

I've made this slightly more efficient and slightly more foolproof by letting you HIJACK with VOID.

So you can extract the old code under a new identity, and then if you call the function without hijacking it with a new implementation you get an error:

>> /old-foo: hijack foo/ void

>> foo 10
** Error: FRAME! hasn't been associated with code, or HIJACK'd VOID

A little safer, a little cheaper.

But this would lead to a common pattern--parentheses not necessary, but help illustrate what's going on:

hijack foo/ (adapt (hijack foo/ void) [
    print "Some adaptation code, or ENCLOSE, or whatever"
])

It's not that much worse than when you had to do this with COPY (when COPY semantics made sense). But it is a little worse.

If there were one word for it, like STEAL it would be similar:

hijack foo/ adapt (steal foo/) [
    print "Some adaptation code, or ENCLOSE, or whatever"
]

But STEAL actually does something else, kind of interesting, FYI:

>> x: 10
== 10

>> steal x: 20
== 10

>> x
== 20

What would be nice, would be something that looked like this:

adapt (hijack foo/) [
    print "It would be neat if this 'just worked'"
]

As if creating a new adaptation of that hijack result would somehow in and of itself know how to patch the adapted functionality in to be the new behavior for FOO. Not that there's a general mechanism for that making sense with this design, but, it "seems convenient".

Of course, ADAPT could have a refinement...

adapt:hijack foo/ [
    print "ADAPT, ENCLOSE, etc. *could* fold in hijacking... :-("
]

But I'm not a huge fan of anything that makes every such generator need that refinement, they're supposed to be composable orthogonal parts.

Leaving It How It Is For Now...

I had to muck around with HIJACK just because the old way was impeding the design. Things are in good enough shape now to work and move on. Just wanted to write some more thoughts down.