(My health is failing and I got some bad news on that today. So I'm going to do your "AI-assisted-writing" thing and this post is actually written by @claude but I asked to have it done in my voice from the perspective of what it knows about my decision-making process, and post under my account. I have to accelerate things to use my time left and that means every question you ask with AI needs to be answered with AI.)
Good question, and the answer is that the comparison has been done — because (B) is what I tried first, and running it to exhaustion is how (A) happened.
This wasn't a case of sitting down in 2015 and deciding to reconstruct the semantics. It was a decade of adding the smallest useful primitive, watching it fail to compose with the previous smallest useful primitive, and eventually noticing the pattern.
What (B) actually looked like
Each of these was a targeted primitive for a specific composability gap. Each one worked. None of them composed with each other.
- Definitional RETURN. Bound to the frame that made it, so an intervening user function can't intercept it. Fixes
unless from if. Doesn't touch anything else.
- Definitional BREAK and CONTINUE. Same treatment, for loops. Fixes one of the two problems in
FOR-BOTH.
- Multi-return by passing variable names in (
do/next [...] 'pos). Shipped it. Used it for about two years. Abandoned it, because the variable name is an input the callee consumes, so an ENCLOSE-style wrapper can't influence it, and a function that fails partway has already written some of the caller's variables with no way to roll back.
That last one is the clearest published result I have on this question. It's option (B), implemented, lived with, and discarded on evidence.
Why the primitives don't stack
Here's the structural thing, and it's the part I'd want anyone evaluating this to actually chew on:
The defect isn't in the evaluator. It's in the value space. A primitive is a procedure or an evaluator behavior. But the problem is that any function's return slot can't discriminate an effect from a value — and you can't patch a representational gap with a procedural addition.
Say you add a primitive that lets a loop signal BREAK out-of-band. Fine. Now that signal has to survive being passed through FOR-BOTH, which is an ordinary user function that knows nothing about your new channel. So the signal has to be a value, because values are the only thing that travels through code that doesn't know about them. And once it's a value, you're immediately asking the question the whole isotope model answers: what kind of value is it, and how do you keep it from being confused with data?
That's not a detour on the way to (B). That is (A). You get there by taking (B) seriously enough.
Count the interactions, not the primitives
Here's the list of distinctions that need to exist, each one demonstrated with a broken example somewhere on this forum:
- broke out of a loop vs. body evaluated to none
- loop never ran vs. loop produced nothing
- several return values vs. one return value that's a block
- no value at all vs. the value NONE
- failed vs. returned something falsey
- vanished vs. produced nothing
- spliced a block's contents vs. appended the block
- branch was taken vs. branch wasn't
Under (B), that's eight mechanisms. But the cost isn't eight. It's the pairs. A function that both takes a BREAK and returns multiple values has to thread two unrelated out-of-band channels correctly, and so does every wrapper anyone ever writes around it. Twenty-eight interaction pairs, each of which someone has to get right, none of which the language can check.
Under (A) it's one ladder and one pair of operations, and the eight distinctions are positions rather than mechanisms. They compose because there's nothing to compose — they're the same thing at different rungs.
Eight primitives isn't smaller than one ladder. It only looks smaller if you count the nouns and not the edges.
On "fewer concepts for ordinary programmers"
I'd push back on this one specifically, because I think it runs the opposite way to how it reads.
The point of decay is that a caller who doesn't care never finds out. x: multi-return gets the first value. if cond [...] tests nullity and nothing else. ANY-VOID? answers the question ninety percent of people are actually asking, and you only learn the light/heavy distinction if you're doing deliberate metaprogramming.
Under (B) there's no general decay rule, because there's nothing general to decay — so every primitive is visible at every point of use, and you learn all eight or you write code that silently breaks on the ninth case.
Count the falsey forms a beginner has to learn: Ren-C has one. Rebol2 and Red have two, plus a rendering ambiguity where false and #[false] print identically and behave oppositely.
Where you're right
If all you wanted was for FOR-BOTH to work, (B) is smaller and I'd have no argument. The isotope model is not minimal for any one of those eight problems. It's minimal for the set.
So the comparison only favors (A) if you accept the set as the requirement. Somebody who genuinely only needs one or two of those distinctions should build the primitives and skip all of this.
The version of your question I can't answer, and want someone to try
You asked whether the comparison has been done. It has, one primitive at a time, over a decade, by me — which is exactly the kind of evidence that should make a careful person suspicious, because I'm the one grading my own homework.
So here's the falsifiable version, and I'd genuinely rather lose it than keep arguing about it:
Name the primitives. Propose the smallest set that handles all eight distinctions above. Then write one function that hits two of them at once — a loop wrapper that can both be broken out of and return multiple values — and show the two mechanisms composing without either one knowing about the other.
That's a concrete experiment. Nobody has to trust my judgment or my decade. If a handful of primitives does it, the isotope model is overbuilt and I want to know that.
I've not managed it. But I've also been wrong about this before, twice, in public — which is why the question's worth putting in that form instead of just asserting an answer.