-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Proposal: Disallow unlabeled blocks as block expressions #9059
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Void-valued expression blocks have a legitimate use-case in #6965. And also in switch statements: switch (action) {
.Inc => { x += 1; y += 1; },
.Dec => { x -= 1; y -= 1; },
else => {},
} |
Those two specific cases should still be allowed, in the same way that block statements are still allowed, but we should disallow unlabeled blocks in switch expressions. Actually there's another use case, which is blocks that have the type const y = some_optional orelse {
logError();
return null;
}; I don't think we can reasonably require a label on this sort of block.
|
I don't like the idea of introducing a formal distinction between switch statements and switch expressions. For example, would the inner switch in the following code be an expression-switch or a statement-switch? switch (flag) {
.Foo => switch (action) {
.Inc => { x += 1; y += 1; },
.Dec => { x -= 1; y -= 1; },
else => {},
},
else => {},
} On one hand, the whole thing is a statement. On the other, the inner switch occurs in an expression context. The current semantics governing this is easy to explain and reason about, while the proposed change would require various special cases and non-local analysis. I'd say this is not worth it given the somewhat exotic nature of the motivating example. |
This question is easily answered from the above rules:
Since the outer switch is a statement, the inner switch is also a statement. |
Makes sense. But that would require introducing separate definitions for SwitchExpr/SwitchStmt, IfExpr/IfStmt, WhileExpr/WhileStmt, etc. into the grammar. If we are still trying to keep it small, I'd say this is a step in the wrong direction. |
We already have this split for If/For/While. Statements allow assignment operations as the payload, expressions do not. Switch is the only block construct that currently doesn't have this split. |
Ok, that's less of a problem than I thought, then. What about the |
The noreturn case would mean that this error can only happen after typechecking. It's almost possible to do the check in astgen, except for functions which return |
So, implementation-wise, unlabeled expression blocks would continue to be accepted by the parser, but then rejected in a separate pass after typechecking confirms that they are indeed void-valued? |
In that case, would it make sense to disallow |
const StringHashSet = std.StringHashMap(void); Also, there are more places where void blocks should be allowed: while (...) : ({ a += 1; b += 1; }){ ... } |
These are all statements (places where you could put an assignment), except |
Ok then, so their arguments are BlockExprStatement and what we disallow is BlockExpr so they are still allowed? Does this mean we need to check for void block expressions in expressions instead of checking all BlockExpr in the AST in a context-free manner? ps: WhileContinueExpr seems like an expression instead of statement:
|
I think it's just named this way in the grammar. It is semantically a statement, since it allows assignment and does not return a value.
Apparently so. The procedure would be to walk down the typed AST and take note of subtrees that are rooted in the RHS of an assignment or in a function argument. These would be treated as proper Expressions rather than ExpressionStatements. Void-valued unlabeled blocks would be allowed in ExpressionStatement subtrees, but would result in an error in Expression subtrees, unless they are Addendum: One place where this could still create problems is metaprogramming. E.g. |
My $0.02, I personally don't see any real footgun here relating to blocks. While this is a funny example of someone not knowing that |
An example similar to this recently came up on the discord:
The author expected it to mean this:
T
is the type of a block expression.tt
is an array of block expressions.when
tt[index]
is encountered, the expression at that index executes.So
getPoint(0)
returns(1, 0)
andgetPoint(1)
returns(0, 1)
Surprisingly, this code compiles, but has a very different interpretation.
T
is void.tt
is nothing, the expressions execute immediately.when
tt[index]
is encountered, it's a void value, so discarding it is not a compile error.So
getPoint
always returns(1, 1)
for any input.I think in general, block expressions which don't return a value are misleading. Some more examples of code like this:
In my opinion, we should require blocks which are expressions to be labeled, and only allow statement blocks to be unlabeled. This would clear up the confusion about the above code, by making it a compile error.
There is one use case for an unlabeled block expression, which is the use of
{}
as the value of typevoid
. There are equivalent expressions, like@as(void, undefined)
andvoid{}
, but this one is particularly succinct. I thinkvoid{}
is sufficient, but we could choose to continue to allow block expressions with no statements inside.The text was updated successfully, but these errors were encountered: