This may deserve the understatement of the year award!
Nevertheless, after many hours of hacking around, I've gotten it to sort of work!!
Overall Use Impressions Are Mostly Positive
Behold:
>> f: make frame! :append
== make frame! [
series: ~
value: ~
part: ~
dup: ~
line: ~
]
>> f.value: 100
>> ap100: anti f ; synonymous in this case with `runs f`
== ~make frame! [
series: ~
value: 100
part: ~
dup: ~
line: ~
]~ ; anti
>> ap100 [a b c]
== [a b c 100]
That's extremely cool. Something to really like about it is how it demystifies aspects of how actions are manufactured, making it easier for people to write their own SPECIALIZE-like and APPLY-like tools in usermode...without relying on voodoo inside SPECIALIZE or APPLY natives!
Or so it would seem.. BUT... a bunch of broken cases draws attention to some of the "hidden" logic that makes specialization not so simple.
For instance: if you create functions like adp: :append/dup/part, then it loses the /DUP and /PART refinements and instead always takes them, having 4 normal parameters. And if you said instead apd: :append/part/dup then APD would also take 4 normal parameters, but with the PART and DUP in a reversed order from ADP. The mechanics which encode this aren't visible in the FRAME!... they're little chains of hidden state. And if you MAKE FRAME! on ADP or APD, then set PART: or DUP: then those chains currently need to be fixed up to know that the fields are specialized out and should no longer be mentioned.
All that has historically been magically covered by SPECIALIZE, which was creating an opaque ACTION! where internally all the bookkeeping was handled for you. If you are just poking values into a FRAME! and then off the cuff running it--expecting it to act partially specialized--then the current data structures can get out of sync and crash.
It is clearly looking like a massive amount of work to rethink this. But I believe the product of that rethinking would lead to more complete and coherent model of FRAME!, making it a more powerful user-exposed facility for building actions.
So I think I'm sold on this. That said, here's some more to say.
Plain FRAME!s In BLOCK!s Execute
This throws a bit of a curveball into how I'd historically segmented FRAME! and ACTION! in my mind...where if you COMPOSE a FRAME! into a block, that it is active:
>> frame: make frame! :add
>> do compose [(frame) 10 20]
== 30
But if we didn't choose this semantic, then since you can't put antiforms themselves into blocks... all invocations of functions would have to be triggered by word references to antiform frames.
e.g. if we left frames inert when encountered directly by the evaluator, you'd always need to go through words-pointing-at-antiforms like RUN or APPLY to get execution:
>> do compose [(frame) 10 20]
== 20 ; let's imagine this falls out, last of three inert things
>> do compose [run (frame) 10 20]
== 30
But then this costs a WORD! lookup and an extra function call, which strikes me as unacceptable overhead to require in order to execute a function. It's almost as bad as requiring a declaration.
>> do compose [let act: ~(frame)~ act 10 20] ; just added: quasicompose!
== 30
It could perhaps be done through some kind of terminal-slash semantic:
>> do compose [(frame)/ 10 20]
== 30
But that's contentious with my terminal-slash APPLY semantics. And since frames can't be put in paths directly, it would have to be put in a group in a terminal-slash path, so more cost and complexity added there.
This is all a rehash of things I dealt with back when antiform actions were first introduced (...and you can read through the stumbling around of that era...). I was a bit uncomfortable that the "inert" form of ACTION! would execute when encountered literally in block evaluation. Sure, it was inert when fetched via word, but was very alive if you composed it in.
Eventually I decided that as long as the key behavior of being fetched via word being inert was met, the semantics when seen literally during a block evaluation didn't matter. After all, WORD!s require a similar caution to be quoted.
This idea of FRAME! executing seems more unsettling, considering how frames have been inert like objects for so long. But when you start thinking that FRAME! has execution potential... like GROUP! and WORD! and GET-BLOCK!, etc... then imagining it executing isn't as much of a stretch. Especially under a new model where it's only an antiform away from running if fetched via word.
Incidentally, if you read the old isotope stumbling around thread, I did mention the idea of antiform frames executing, July 2022:
What if it's only antiform ACTION!s that execute implicitly? And what if you can make an antiform FRAME! that does the same?
How big of an issue this really is depends on how often people compose FRAME! into blocks and execute them when they -DON'T- intend to run them. The main case to worry about is the MAKE OBJECT COMPOSE [...] scenario, but I've pointed out how you've got to fret over GROUP! and WORD! and PATH! and any other evaluator-active types.
I think it's acceptable to have FRAME! be another evaluator-active type. And people who aren't used to it being anything else won't see it through the same lens I'm seeing it now. Maybe they'll think "hey, it's one antiform away from being WORD!-active, why wouldn't it run?"
MESSY MOLDING
This isn't an entirely new problem, as historical Rebol always had a problem rendering functions.
But Ren-C had been molding ACTION!s compactly as just the cached name of the action (from the last SET-WORD! it was used with), along with a compact list of the parameters. However, FRAME!s were molded like ordinary objects.
Now it's a bit of a quandary to decide how to do the rendering. We could say that if the console is showing you an antiform FRAME!, then it does a light rendering looking like a historical action... but if the frame is normal, then you get the fields. Which is something I'm probably going to do shortly. If QUASI-FRAME!s were given this treatment as well, most actions would appear compact.
Anyway, like I say, not particularly a new problem. The limits of a text console really bite us here...maybe the web REPL can do better.
MAKE FRAME! ACTION OF BINDING OF
Consider this case of leveraging the linked relationship between an action and a frame:
foo: func [x] [
f: make frame! action of binding of 'return
probe f
]
Now let's say you call it with foo 1020. I would expect f.x to be unset, because what we effectively asked for was to make frame! :foo
But if there was no such question as ACTION OF, and you instead wrote:
foo: func [x] [
f: make frame! binding of 'return
probe f
]
This would mean MAKE FRAME! is taking a FRAME! as input, and in that input frame, X is 1020.
Is it clearly implied that all the variables should be zeroed out?
If this were the case, we'd expect the same from MAKE OBJECT! of an existing object...to get the fields and not the values:
>> obj: make object! [x: 10 y: 20]
>> obj2: make object! obj
== make object! [
x: ~
y: ~
]
That's not how Red works...it seems to be a synonym for COPY OBJECT. FWIW, neither Rebol2 nor R3-Alpha permit it.
Debugger Queries: "Find Any Calls to Action X On Stack"
There was an old feature in the now-very-defunct-debugger, which allowed you to look above you for stack levels that were "instances of a specific function".
This would be defeated by specialization. So let's say you made a specialization of append that fixed the value at 5:
ap5: specialize :append [value: 5]
Calls to this function would not match as calls to APPEND. They're calls to ap5--and the fact that there's a relationship between AP5 and APPEND would be wiped out.
But now, it's sort of defeated in all cases... because there's not a concept of ACTION OF.
However, maybe this is something that can be worked through in a better way. A frame with fields filled in, turned into an antiform, is enough to be executable. So if you want to make a specialization now, you don't go through the opaque step of forming a new action. The only time you create opaque levels is if you do something like an ENCLOSE or ADAPT.
So there could be something like an "action" in the sense of identity, that you could ask for the "outermost function generator in charge". It would give the same answer for :append and :ap5 in the new model. It's kind of like an answer to BODY-OF.
HIJACK Semantics
HIJACK is one of those things in the system that really throws a wrench into just about everything. The design is about as clean and clever as I think I can make it, but every time I meddle in the frame and action mechanics something related to HIJACK trips up.
Unfortunately we are now completely dependent on it working, as it's how things like the ReplPad hook into providing its own handling for PRINT and such.
In any case, the granularity for HIJACK was on ACTION!s... not FRAME!s. So with the two unified into one umbrella idea of a FRAME!, we're only able to hijack "legacy-action-like" FRAME!s and not arbitrary "context-like" FRAME!s. We can offer an operator to turn a context-like frame into a legacy action frame (you could just ADAPT it with an empty block and get one)...and you could then hijack the result of that. But you can't specifically hijack the context-like FRAME!s.
It may be that this can be solved with some kind of magic, but I haven't wrapped my head around it, and there are a zillion other things to be solved first. Suffice to say that the black art of hijacking just got a little darker.
This Is The Tip Of The Iceberg, BUT...
...I still think it's a winner. Again... when I look at this, I feel satisfied... the annoyance of ACTION! vs. ACTIVATION! is all washed away:
foo: function [frame [frame!]] [if true [run frame arg1 arg2]]
bar: function [action [action?]] [if true [action arg1 arg2]]
baz: function [frame [frame!]] [let action: runs frame, action arg1 arg2]
Conceptually, getting to a point where "ACTION" and FRAME! are seen as two sides of the same coin seems coherent: a frame needs to encompass everything you need to know about an action in suspended animation. I may not be able to get that fully sorted right now, but it points a compass toward how to make decisions in the future.
But it's likely going to force some big internal name changes. I'd been calling the stack levels internal to the system juggled by the stackless trampoline "Frames"...because whenever you extracted an object-like thing from a FRAME! you'd get it as a "Context". But now, the design is necessitating a fusion of the guts of what was an "Action" with a "Context" to make a new abstraction...and it seems fated that this abstraction needs to be called a "Frame".
Right now, I'm reusing "Action" as the name of the fused entity...and saying that when it's "legacy-action-like" it's a "Phase", but when it's context like it's an "Exemplar". But naming disconnects like this create a lot of problems.
So I'll likely rename the internal trampoline entities "Level", and give "Frame" to be what is held by FRAME! values. There's actually some good arguments for why Level is different from what you'd think of as a traditional "stack frame". I don't mind it, but it's just a lot of churn...(oh, why do we store source code in ASCII still?)