Skip to content

Instantly share code, notes, and snippets.

@JoshCheek
Last active April 11, 2023 14:11
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JoshCheek/5625007 to your computer and use it in GitHub Desktop.
Save JoshCheek/5625007 to your computer and use it in GitHub Desktop.
void value expressions make no sense
-> { a = case when true then return end } # this is allowed
-> { a = if true then return end } # this is not
-> { a = if true then return; 2 end } # this is
-> { a = (true && return) } # this is allowed
-> { a = (return && true) } # this is not
-> { a = begin; return
rescue; return
ensure; return
end } # this is allowed
-> { a = begin; return; end } # this is not
@fj
Copy link

fj commented May 22, 2013

The problem is that you are trying to assign an incomplete value to an object (in this case, a), and the parser is complaining that it doesn't know how to generate the correct value to assign. This is different than a nil value, and is always an illegal parsing error. Notice that if you don't perform an assignment, then there's no error:

# This is a legal expression.
-> { if true then return end }.call()
# => nil

Whenever the Ruby parser doesn't get a chance to finish resolving the expression completely to decide what the ultimate value should be, and when it also decides that it needs to know that to continue interpreting, that's when you wind up with the void value problem.

This occurs in two situations, both outlined in parse.y in the Ruby code. Your examples only demonstrate one of the two, so I'll focus on that case.

Here's the relevant section of parse.y inside value_expr_gen, which is a method that tries to construct a value from an expression, which is needed if you want to assign the value to something, as in your case.

 8818     case NODE_RETURN:
 8819     case NODE_BREAK:
 8820     case NODE_NEXT:
 8821     case NODE_REDO:
 8822     case NODE_RETRY:
 8823       if (!cond) yyerror("void value expression");
 8824       /* or "control never reach"? */
 8825       return FALSE;

Notice that Ruby checks to see whether you're inside a conditional expression (if ...) or chain (... && ...) before deciding if it's a void value expression or not. That's why return && true is illegal (because you haven't finished the conditional) while true && return is legal (since you have).

Similarly, -> { a = if true then return; 2 end } is legal because the value of the if node ends with 2 rather than the return, so the error doesn't trigger -- the result of the if node is the node 2, rather than the node return, which triggers the parser failure. Of course, the value "2" will never be assigned or used here; remember, we're in the parser here, not the interpreter.

@JoshCheek
Copy link
Author

I know what a void value expression is, I constantly have to work around it in Seeing Is Believing.

I'm saying it is inconsistent where it will be raised, and there is no rule that will allow you to identify when it will be raised, beyond experimentation / reading the implementation. Mostly I point this out because it also illustrates the unnecessity of void value expressions (they could just stop doing that and all known bugs in my gem would go away).

To your points. still don't understand how you think this is consistent. In a = (true && return) the boolean simplifies to the RHS: a = return, which is a void value expression. You're saying that true && return is allowed because you've finished evaluating the conditional, but then a = if true; else return end should be allowed, because you've finished evaluating the if statement. But, the if statement inherits the voidness of the return statement, where for no clear reason, the && does not (it must be an implementation detail). Thus you can assign the true && return but not if true; else return, which makes no sense.

Your having to go to the implementation to explain it is evidence that there is no clear rule, of course, I'd already experimentally arrived at the same conclusion.

And none of this does not addresses why the if statement inherits the voidness, but the case statement does not.

Lets face it, it's an inferior attempt at unnecessary static analysis that leads to incongruent results. this is not to say that it is valueless, I think there may be some very small amount of value. However, I don't think its value exceeds its cost, and I don't think it makes any sense.

@fj
Copy link

fj commented May 23, 2013

I know what a void value expression is, I constantly have to work around it in Seeing Is Believing.

I'm sorry; I didn't intend for that to sound condescending! I was just explaining for the benefit of someone reading who might not know.

but then a = if true; else return end should be allowed, because you've finished evaluating the if statement.

You haven't finished evaluating the if statement, just the conditional portion (that's what the cond flag checks; see here). The body of a node must also be evaluated by the parser before it can keep going, and the body here is a return node (NODE_RETURN), so you get the void value expression problem when control reaches line 8818 in parse.y.

Mostly I point this out because it also illustrates the unnecessity of void value expressions

100% agree that they seem unnecessary. I'm not sure why that level of static analysis is done in the parser. This is probably a question for the ruby-lang list.

Your having to go to the implementation to explain it is evidence that there is no clear rule, of course, I'd already experimentally arrived at the same conclusion.

The rule to me is "if you try to assign from an expression whose parser-evaluated value is a node that causes a change in control flow, and you're not in the middle of evaluating a conditional node, then you will get a VVE".

(they could just stop doing that and all known bugs in my gem would go away).

I'm not a SIB user so I haven't studied the code, but that misses one other case where a VVE can be raised -- when a method definition is a VVE. Could that account for some of your bug cases?

I'm definitely curious about why VVEs exist in the parser at all. There's also only three tests for them in RubySpec and none of them cover the case you describe. So they seem to be not very important. Maybe it's worth proposing a patch to ruby-lang?

@sdegutis
Copy link

The real problem is that Ruby is an insane language and you're still using it.

@jaswinder97
Copy link

Hello @sdegutis !!
how can you say Ruby is insane language ?

Don't just make unusable comments .

@icefapper
Copy link

Lets face it, it's an inferior attempt at unnecessary static analysis that leads to incongruent results. this is not to say that it is valueless, I think there may be some very small amount of value. However, I don't think its value exceeds its cost, and I don't think it makes any sense.

You have cleanly nailed it; to my complete surprise, there are some devs out there that still confuse a void-value-expression (an expression with no inherent value) with a void-value-expression-error (an error caused by mixing so-called "value expressions" with "void-expressions").

Interestingly enough, the most basic forms of void-value-expressions are break, next, return and raise, all of which, intuitively, are used to change the natural path of execution in some way. To me, that has been the reason of the dichotomy between value-expressions and void-expressions -- to prevent things like 12 * break or 12 * (return 12).

To a sane mind, a void value expression error is raised only when the value of a void value expression is needed to resume the execution of the program in some way:

  • Consider 12 * break: the value for break is needed to calculate the value for 12 * break, and that is where the error occurs.
  • Now consider this: break && l() -- the value for break is needed to decide whether l() should execute. this, too, is an error.
  • On the other hand, consider 12 && break -- the value for 12 is needed to decide whether we should proceed to executing break -- which makes complete sense to be honest, because, even though we had a break, its value was not needed.

Now consider this: (12 && break) && return. Sanity-wise, the reasoning that supposed to lead us through executing it would be:
* the value for 12 is needed to decide whether break should execute (nothing wrong with that!)
* the value for 12 && break is needed to decide whether return should execute (12 && break has no inherent value -- it's not even nil, it must be thought of as a "compound" void-expression, and at this point a "void-value-expression-error" must be actually raised!)
** BUT IT WON'T **
That to me is nothing but poor and insane implementation of a feature that could have had a slight value.

Ruby as a language is not insane (it's a mere description), but the way its standard implementation behaves makes me run for cover occasionally. tl;dr Checks performed for hunting void-value-expression-errors are half-baked and are, at their best, useless, at least the way they are implemented currently.

@kaline
Copy link

kaline commented Jan 15, 2018

Ruby is insane, because is genial!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment