UPARSE was an early client of multiple return values, at a time when they worked by assigning variables local to your frame, which were then proxied to items in a SET-BLOCK!:
multi-returner: func [
return: [integer!]
secondary: [integer!] ; SET-WORD! indicated another return
][
secondary: 20
return 10
]
>> [ten twenty]: multi-returner
== 10
>> ten
== 10
>> twenty
== 20
This basically made every multi-return function a kind of infix operation, that was able to take a SET-BLOCK! on its left hand side. (In fact, it was prototyped using infix.)
But this method had composability problems, and was defeated by abstraction of any sort, even the most minor forms:
>> [ten twenty]: (multi-returner)
** Error: even this wouldn't work
So the method gave way to returning antiform BLOCK!s. These represented parameter packs that would "decay" to their first item in most circumstances...but SET-BLOCK!s were one of the cases that could pick them apart (though you could design other operations as well).
multi-returner: func [
return: [~[integer! integer!]~]
][
return pack [10 20]
]
You can read all about it in The History of Multi-Return in Ren-C
So Local Proxies Died...But UPARSE Mimicked Them
Just because the mechanics got rid of local proxies doesn't mean you can't fake them. All you have to do is hack up its RETURN function to make a PACK using a local variable.
Simplified example:
proxy-multi-func: adapt func/ [
body: compose '[
return: adapt return/ [
^value: pack [^value secondary]
]
(as group! body)
]
]
multi-returner: proxy-multi-func [
return: [integer!]
{secondary} ; could be specially marked, if spec rewritten
][
secondary: 20
return 10
]
So when the multi-return-by-antiform-block change happened, this is what COMBINATOR did instead of transition to having every combinator do return pack [synthesized remainder]
Instead it worked the same: you'd update the input position however you wished, do return synthesized. Except now the specialization of RETURN would PACK things up for you.
Why Did COMBINATOR Preserve Proxying?
Well... for starters, to show that it could be done. You should be able to do it. So having a living test case to hammer through any issues was good.
Also, because some combinators have two return values (synthesized and updated input), while others add a third (pending). In truth the combinator always needs to return a pack of 3, it's just that some combinators automatically pipe the pending results from successful combinators to the output. This means even if your combinator returned a pack of 2 in the piped case, that would have to be broken apart and turned into a pack of 3. Having it in components helps.
But generally, I think it makes the code clearer as well. Saying (return pack [x y]) doesn't have any labeling, while (remainder: y, return x) is somewhat clearer, and you don't need to label the "primary" result because that's understood as what the combinator is synthesizing.
Anyway, Just Wanted To Sum Up UPARSE RETURN
I was questioning it, and wanted to kind of work through why it is the way it is. But I think it's right.