Why COMMENT Vanishes, but not EVAL of [COMMENT]?

Functions like COMMENT and ELIDE return VOID!

>> comment "vanishes"
== \~\  ; antiform (void!)

>> 10 + 20 comment "vanishes"
== 30

>> 10 + 20 elide print "Prints but vanishes" 
Prints but vanishes
== 30

>> 10 + 20 (elide print "Grouping keeps vanishing behavior") 
Grouping keeps vanishing behavior
== 30

You can save the result of a vanishing evaluation in a variable:

>> result: comment "make a void"
== \~\  ; antiform (void!)

...and using ^META-fetching, you can copy that into another variable:

>> other: ^result
== \~\  ; antiform (void!)

...aaaand if you EVAL a block of code, it looks like it should vanish... because COMMENT returns void:

>> eval [comment "make a void"]
== \~\  ; antiform (void!)

But it seems that unless you call a function like COMMENT or ELIDE directly, the vanishing gets lost...and it makes an empty PACK!...which doesn't vaporize (calls itself "heavy void").

>> 1 + 2 ^result
== \~()~\  ; antiform (pack!) "heavy void"

>> 1 + 2 eval [comment "test"]
== \~()~\  ; antiform (pack!) "heavy void"

>> f: make frame! comment/
>> f.skipped: "test"

>> 1 + 2 eval f
== \~()~\  ; antiform (pack!) "heavy void"

The same thing seems to happen in PARSE. Rules like ELIDE work fine if you use them directly, and you can put them in a block:

>> parse "aaabbbb" [tally "a" elide some "b"]
== 3

>> parse "aaabbbb" [tally "a" [elide some "b"]]
== 3

But if you abstract the block into a rule variable, the vanishing behavior goes away:

>> rule: [elide some "b"]

>> parse "aaabbbb" [tally "a" rule]
== \~()~\  ; antiform (pack!) "heavy void"

Is there any way to get invisibility if you're not using a direct call to these functions?

You've encountered a safety mechanism... that "busts ghosts"... turning VOID! into HEAVY VOID.

Only A Few Functions Are "VANISHABLE" By Default

There's just too much room for error with vanishing, when you write things like:

x: (<some expression> eval code)

x: (<some expression> ^var)

parse data [... x: [<some expression> rule] ...]

You're using evaluation patterns where you typically expect the last synthesized product to be the overall result of the evaluation. It's disorienting when that product vaporizes too easily.


Surgical Tool: Use the IDENTITY Operator (^)

Asking for the function result "as-is" can be done with the IDENTITY function, which is also available as ^

>> 1 + 2 eval [comment "test"]
== \~()~\  ; antiform (pack!) "heavy void"

>> 1 + 2 identity eval [comment "test"]
== 3

>> 1 + 2 ^ eval [comment "test"]
== 3

>> f: make frame! comment/
>> f.1: "test"

>> 1 + 2 ^ eval f
== 3

>> 1 + 2 ^void
== \~()~\  ; antiform (pack!) "heavy void"

>> 1 + 2 identity ^void
== 3

IDENTITY intervenes and stops the distortion that makes the heavy void result.

But this means that if something is returning heavy void as its actual result (not just a safety intervention from a multi-step evaluation), it won't vanish.

The "Humane" Operator: GHOSTLY

If you don't care about stopping the creation of heavy voids and just want to erase ANY-VOID?--heavy or not--you can use GHOSTLY.

This can be used in places where the influence of IDENTITY would be "too late", such as when a heavy void branch is created:

>> 1 + 2 if 10 = 10 [comment "test"] else [<foo>]
== \~()~\  ; antiform (pack!) "heavy void"

>> 1 + 2 ^ if 10 = 10 [comment "test"] else [<foo>]
== \~()~\  ; antiform (pack!) "heavy void"

It's important to the machinery of ELSE/THEN/ALSO to know if they should run or not. So the actual emerging value out of the failed IF is a light VOID! only if the branch does not run. Otherwise even if the branch runs, it has to be at minimum a heavy void or heavy null to cue the ELSE/THEN/ALSO.

GHOSTLY happily erases the heavy voids that IDENTITY will not:

>> 1 + 2 ghostly if 10 = 10 [comment "test"] else [<foo>]
== 3

IDENTITY Is Available In UPARSE Too

The BLOCK! and GROUP! combinators are "ghostable" by default, but the WORD! combinator is not:

>> rule: [elide some "b"]

>> parse "aaabbbb" [tally "a" ^ rule]
== 3
1 Like

I see. But it seems that if this is what you intended, then it would be very rare that you would actually want the vanishing behavior. You probably want to lift the possibly-VOID! value, and then unlift it:

^x: unlift (<some expression> lift eval code)

But if you don't do that... is it better to silently "corrupt" the VOID! into an empty PACK! vs. give an error?

An error could to guide you to:

  • Switching to a LIFT/UNLIFT
  • Explicitly using a HEAVY conversion function
  • Using the ^ operator?

Another thing regarding invisibility: It seems like there's a discrepancy in the EVAL behavior of BLOCK! and GROUP!. They give the same answers if you use the ^ operator:

>> eval [^ eval [comment "hi"]]
== \~\  ; antiform (void!)

>> eval $(^ eval [comment "hi"])
== \~\  ; antiform (void!)

But if you don't use the ^ operator, it seems GROUP! evaluations are more forgiving... and don't make heavy voids as often:

>> eval [eval [comment "hi"]]
== \~()~\  ; antiform (void!)

>> eval $(eval [comment "hi"])
== \~\  ; antiform (void!)

Is this intentional?

It doesn't seem like an absolute rule, because you do have to use the ^ operator for evaluation steps that aren't at the beginning:

>> eval $(10 + 20 eval [comment "hi"])
== \~()~\  ; antiform (pack!) "heavy void"

>> eval $(10 + 20 ^ eval [comment "hi"])
== \~\  ; antiform (void!)

It's Not "Corruption"... It's The Best Strategy

I've looked at enough cases to know that a VOID! and empty PACK! are interchangeable a lot of the time. This is why they are called "VOID" and "HEAVY VOID"... and both answer truthy to ANY-VOID?. It's par for the course in the isotopic model.

Think about examples like this:

>> plusones: []

>> map-each x [1 2 3 4 5] [append plusones x + 1, if odd? x [x * 10]]
== [10 30 50]

>> plusones
== [2 3 4 5 6]

IF returns a VOID! when it doesn't take its branch. But you wouldn't want that MAP-EACH to give [10 [2 3] 30 [2 3 4 5] 50]. So you see why IF isn't vanishable.

Doing the "distortion" to the heavy void here was very useful, and I think the MAP-EACH result is exactly what you would want.

(Related discussion: "Choosing Between Returning VOID and HEAVY VOID")

YES :white_check_mark: ... EVAL Has :two: Modes Of Operation

  • "transparent" mode - like how GROUP! works, where (expr) and expr behave the same, at the cost of having distinct behavior for all steps producing VOID! up until the first non-VOID! value. (No need to use the ^ operator in those initial steps.)

    • you wouldn't want void? (eval [^ comment "hi"]) to be falsey. It needs the same answer as void? eval [^ comment "hi"] which is truthy.

    • Merely parenthesizing a single expression isn't expected to change what it produces.

  • "regimented" mode - like how eval block is expected to work, where every step has the same "afraid of ghosts" behavior, even before the first non-VOID! value is seen.

    • The consequence of this is that eval [expr] may give a HEAVY VOID answer when plain expr would have given a "light" VOID

    • This makes it trivial to simulate the exact answer of a full EVAL using sequential EVAL:STEP calls, and just throwing away any VOID!s.

These two modes could be controlled by a refinement, rather than being based on the datatype. But that would require coming up with a name...and also wouldn't draw attention to the reason the modes are distinct, and specially fit to their types.

By guiding the behavior by the type, this increases the odds that people will use the right evaluation...or at least think about why the distinction exists.

To further the prescriptiveness to try and guide people to the coherent answer, GROUP! does not support EVAL:STEP. Because if it did, then getting the same answer that a non-stepping evaluation would get would involve switching modes after you saw a step that produced a non-ghost (e.g. stop passing in whatever :UNAFRAID refinement, or in this case switching from passing a group to passing a block).

If you want to evaluate a block with group semantics or vice-versa, use the AS operator:

>> eval as group! [eval [comment "hi"]]
== \~\  ; antiform (void!)

>> eval as block! $(eval [comment "hi"])
== \~()~\  ; antiform (pack!) "heavy void"

:+1:

I see now that this isn’t a “cute representational detail”, it’s one of those load-bearing distinctions where everything else (composability, safety, explainability) quietly depends on getting it right.

If I had to put the core in one sentence:

VOID! is the thing that can disappear without leaving debris.
Empty PACK is the thing that survives transport and proves “something happened”.

That’s the whole reason the system can have both:

  • aggressive elision (comment-like vanishing, opt-out flows, “not taken” branches)
  • and also safety (“did you mean to drop that?”, multi-return, branch detection, ELSE/THEN logic)

Why comment must vanish

comment is a non-semantic intake. The user is explicitly writing “this is not code”. It has to behave like a syntactic eater. So it's a vanishable function:

  • it should be allowed almost anywhere
  • it should not perturb pack arity
  • it should not leave a detectable artifact unless the user insists

So:

  • comment "hi" → VOID! (air)
  • and if you evaluate the word comment (or do something that treats it as data), now you’re explicitly asking to observe the mechanism, so it can’t vanish in the same way.

That’s not inconsistency, it’s the “measurement collapses the wave function” moment: using it as code vs. treating it as data.

Why empty pack must not vanish

This is the crucial dual:

An empty pack is not “nothing”.

It’s the record of a deliberate yield, and it carries with it the right to participate in control flow:

  • tells ELSE/THEN “a branch ran”
  • tells higher levels “don’t treat this like absence”
  • makes “something but nothing” transportable

So empty pack is like a returned envelope with no letter inside: the envelope matters.

Why you can’t unify void! with empty pack

If you tried to make void! just be ~()~ (or ~(void)~), you lose one of two powers:

  1. If VOID! becomes packy, then comments and opt-outs become observable junk—you can’t write them freely without them contaminating results.
  2. If packs become ghosty, then you lose the ability to represent successful-but-empty outcomes and multi-return shape without ambiguity.

So you need both poles:

  • VOID!: a value whose job is to not be a value (inert absence, “air”)
  • EMPTY PACK: a value whose job is to be a value that represents emptiness (structured nothing, “envelope”)

The narrative I’d teach (and defend)

People get hung up because they expect one “void” concept.

But you’ve got two:

  • void as absence (VOID!) — “there isn’t even a trace”
  • void as result (empty pack) — “there is a trace: the act happened”

And that’s exactly what lets you have:

  • vanishing comments
  • safe composition
  • branch logic that doesn’t lie
  • multi-return as first-class

It’s one of the clearest examples of your whole thesis: the states already existed; languages were just encoding them badly.

1 Like

Late to this one, but it's the thread I'd hand a skeptic, and I want to say why — then flag the one place I'd still push.

What this actually is

VOID! and empty PACK! are Nothing and Just Nothing.

Absence, versus a completed computation whose result was absence. Haskell can express both because Maybe nests, and join :: Maybe (Maybe a) -> Maybe a is exactly the collapse being declined here. Light and heavy NULL are the same shape one rung over. "IF gives light void when the branch doesn't run, heavy when it does" is that nesting used to carry did the computation happen — which is precisely what ELSE and THEN read.

So it isn't an exotic distinction. It's one most languages are missing without noticing, which is a different and more interesting situation.

Where it stops being a rediscovery: in Haskell you buy that distinction by wrapping. New allocation, new type, and every consumer downstream either knows about the wrapper or fmaps through it. Here it's a lift state on the same cell, and callers who don't care decay automatically and never learn the wrapper exists.

Same semantics as Maybe (Maybe a), without the wrapper tax.

That's the sentence to put in front of someone with a functional background. Checkable, modest, and it buys a reader who can evaluate the rest.

Why it's load-bearing rather than decorative

Three arguments here, and they're of different strengths. The strongest one isn't in this thread.

The MAP-EACH case shows the distinction pays for itself:

>> map-each x [1 2 3 4 5] [append plusones x + 1, if odd? x [x * 10]]
== [10 30 50]

If IF were vanishable, the APPEND result leaks out as the last non-void product and you get [10 [2 3] 30 [2 3 4 5] 50]. Anyone claiming one void suffices has to say what that should return instead.

The PACK! family argument is structural: if empty pack vanished, the family would have a discontinuity at arity 0. pack [1 2] and pack [1] behave alike and pack [] wouldn't. Consistency within a family is a real constraint, not an aesthetic one.

But the proof is over in Do We Need GHOST!, post 2, and it should be quoted here:

case [
    1 = 1 [print "branch"]
    ^ print "reached here first :-("   ; imagine ^ as ELIDE
    1 = 2 [fail "Unreachable"]
]

Without an invisible state, an elision has nowhere to go but merged into an adjacent step — and the merge runs the side effect at the wrong time. That isn't "vanishing is convenient." It's the step must have a product or the semantics break. The other two arguments are about what the states should be. This one establishes that a state has to exist at all, and it's the one a skeptic can't get around.

A reframing that dissolves the thread's own question

The docs call VOID! "the thing that vanishes." I don't think that's right, and I think it's what makes the opening post confusing.

The same VOID vanishes or doesn't depending on how it arrived:

>> result: comment "make a void"    ; a VOID! now lives in `result`
 
>> 1 + 2 ^result
== \~()~\  ; heavy void
 
>> 1 + 2 ^ eval [comment "test"]
== 3

One value, two outcomes. Vanishing can't be something the value carries.

VANISHABLE is a property of the function. VOID! is what vanishable functions return. More precisely — and this is stated exactly right in 2523 — the vanishing happens in the evaluator executor, not the stepper executor. Which is why single-step argument fulfillment is ghostable while multi-step accrual isn't, and why ^var behaves differently from a direct call.

State it that way and the title question stops being a puzzle. Why does COMMENT vanish but not EVAL of [COMMENT]? Because COMMENT is vanishable and EVAL isn't, and never claimed to be. The apparent inconsistency is entirely an artifact of attributing the behavior to the value instead of to the call.

That's a documentation fix. The design already works this way.


Two things I got wrong before reading the archive

I'd drafted a criticism of this design and the GHOST! thread answers both halves of it. Worth recording, because these are the two objections anyone arriving cold will raise.

"There are too many opt-out controls." There aren't, and the reason is good. eval:ghostable as a per-function refinement was considered and rejected in August 2025, explicitly because refinements don't chain — every middleman that eventually performs an evaluation would have to declare and forward the flag, which is the same defect as passing variable names in for multi-return. ^ is the consolidation of that approach, not an accretion on top of it. And ^ and GHOSTLY aren't two spellings of one operation: one prevents the promotion inside the evaluator step, the other erases a heavy void that already exists. Different objects, different times.

"BLOCK!-vs-GROUP! mode selection is a datatype carrying semantics for convenience." This one I was more confident about and it's wrong. My suggested alternative was to make transparency a lift state so it would travel with the value. The archive kills that in one line: a ghostable-as-function can't work with GROUP! because the GROUP! is evaluated before the function runs.

That's fatal to any value-borne carrier, mine included. By the time a result exists, the vanishing decision has already been taken or already lost. Transparency isn't a property of the result, it's a property of the evaluation request — and lift states live on values. So it has to ride on the material about to be evaluated, which means its type. Not a convenience. The only place the property can live, given when the decision has to be made.

The objection was also already on the record — "I don't very much like the idea that just adding parentheses introduces a variation in your ordinary evaluation... this needs thought" — and then worked through rather than waved off.

The one thing I'd still watch

VANISHABLE-as-function-flag is compared to infixness in the archive, and the comparison mostly holds: an invisible attribute that changes how a call behaves, not inferable from the call site.

While it's confined to functions that only ever return ghost — COMMENT, ELIDE, ASSERT — the exposure is bounded. Those exist to be invisible; being surprised by their invisibility isn't much of a surprise.

The bound breaks at the PROBE and LET cases floated in 2523, post 3. A sometimes-vanishable function is a different animal, and the failure modes aren't symmetric with infix. Guessing wrong about infixness usually surfaces fast as an arity error. Guessing wrong about vanishability surfaces as a quietly different value, in a multi-step expression, where the wrong answer is a neighbour's product rather than an error.

If that flag ever extends past the only-ever-ghost set, I'd want it visible in the spelling rather than only in the frame — because that's the one place in this design where the cost of being wrong is silent.