~sircmpwn/hare#822: 
Generalize labellable expressions

Currently only compound expressions are labellable. The best way to label the result of a match or switch expression is something like this:

let x = switch (...) {
case ... => :label {...};
case ... => :label2 {...};
};

and you have to create a new labelled compound expression for each case.

Instead, labels should be able to be used on any expression, not just compound expressions. Like this:

let x = :label switch (...) {
case ... => ...;
};
let y = :label if (...) ... else ...;
let z = :label for (...) {...};
Status
RESOLVED CLOSED
Submitter
~autumnull
Assigned to
No-one
Submitted
1 year, 1 month ago
Updated
6 months ago
Labels
design harec spec

~sebsite 1 year, 1 month ago

Should pin down the exact semantics here. For compound expressions, and control-flow expressions like if, for, switch, match, this makes sense. But there's plenty of other expressions where this could be allowed, but the behavior wouldn't be obvious. Does this just always change the expression result type?

:foo yield :foo, 0; // the yield itself has a result of int?
:foo let x = {
    if (true) yield :foo, 0; // yields out of binding expression; does the binding expression yield an int? what's the value of x?
};

The two main use-cases for something like this I can imagine would be allowing yielding from for-loops, and yielding from switch/match expressions, both of which would be very nice to have. But I'm skeptical that generalizing labellable expressions is the right way to go about this. I'm not explicitly against this proposal though, so long as the quirks are figured out.

~autumnull 1 year, 1 month ago

~sebsite yeah you're right, i don't really mean "any expression". The expressions that you listed (for, if, switch, match, compound) are the only ones where this makes sense, i think in any other situation it should be an error.

~sebsite 1 year, 1 month ago

What benefit does this have then over just adding special syntax to switch/match?

switch (...) :label {
...
};

Current language constructs work on labelled blocks, so if this were implemented, these would either need to be redone to work on other labelled expressions, or there would be multiple ways to label things, neither of which I'm a huge fan of.

// would these two be semantically equivalent? or would one of them be disallowed?

let x = for () :label1 {
    break :label1, 0u8;
};

let x = :label2 for () {
    break :label2, 0u8;
};

~autumnull 1 year, 1 month ago

imo the prior of those two examples would be disallowed, because the label is a label for a compound expression, not a for expression, so you can't break from it. The latter would be the right thing to do there.

A better example for the first one would be

let x = for () :label1 {
    yield :label1, 0u8;
};

and in this case i don't think yielding anything except void from the body of a loop makes sense and this should probably be an error, since conceptually, the body is the result of one iteration of the loop. This would be equivalent to

let x = for () 0u8;

which is not really sensical.

~sebsite 10 months ago

It's been a little bit, and I figured I'd write down my current two cents here to hopefully maybe get a discussion going or something:

On the surface, generalizing labels to all expressions sounds like the right thing to do. But I don't think it actually is. As ~autumnull said, it really only makes sense in compound, if, for, switch, and match, and if can even be removed from that list since compound expressions take care of that. Outside of that list, though, labels are at best useless, and at worst semantic nightmares, as in the two examples in my first comment.

So, if they're not generalized to everything, then I want to be a bit picky for exactly where labels will be allowed. I don't think they should be allowed on if expressions, for instance, since that would duplicate the behavior of compound expressions.

Part of me doesn't mind the current semantics of breaking from a labelled for-loop body, but another part of me acknowledges that the semantics are very weird: break doesn't operate on compound expressions, it operates on loops. But yet selecting the loop to break from is done by using the label of a compound expression. So I think it's reasonable to allow for-loops to be labelled. Though it'd be nice to keep the rule that yield always operates on compound expressions, and break/continue always operate on loops, so e.g. you wouldn't be allowed to yield from a for-loop, and you wouldn't be allowed to break from a compound expression. I can see why people wouldn't like this, since it kinda feels like an arbitrary separation for what's pretty much the same operation, but break and yield already exist as separate things, and neither is going away, so I'd rather embrace their differences than make them identical when used with a label.

switch and match desperately need a way to be labelled. I have no strong opinions on the syntax we use here, but if I had to pick I'd say I prefer switch (0) :label { to :label switch (0) {, since the former makes it more clear that the label refers to the implicit compound expression of each case, rather than the switch expression itself (this distinction matters if we want to keep the rule that yield only operates on compound expressions). I could be persuaded otherwise though (and will very quickly concede if others disagree with me here), especially if we'd rather keep the syntax consistent across all expressions which can be labelled.

So, tl;dr:

// compound label
:label { yield :foo, 0; }; // must be yield, not break
// for label
:label for (true) { break :foo, 0; }; // must be break (or continue), not yield
// yielding to a labelled for-loop body is equivalent to continuing the loop itself
for (true) :label { yield :label; };
:label for (true) { continue :label; };
// switch label
switch (0) :label { case => yield :label, 0; };
// or
:label switch (0) { case => yield :label, 0; };
// ditto for match

~autumnull 10 months ago

I don't think they should be allowed on if expressions, for instance, since that would duplicate the behavior of compound expressions.

it's not quite duplicating the behavior of compound expressions, since there's an else branch, see:

if (...) :label1 {
	...
} else :label2 {
	...
};

requires an extra label compared to

:label if (...) {
	...
} else {
	...
};

the latter of these is far more legible imo, and makes it clearer what you're actually doing (yielding from the if/else statement)

keeping break/yield separate sounds fair enough, no strong opinions

i prefer the label as a prefix to the switch/match keyword, for consistency with for and compound expressions. i also think it's simpler to think about as yielding from the switch expression itself, similar to yielding from a chain of if/elses.

~sebsite 8 months ago

Something I'm just now realizing is that this grammar isn't LL(1), since for loops (and if expressions) are in a different expression class than compound/switch/match expressions. compound/switch/match expressions exist within unary-expression, whereas for/if are in the high-level expression class. The reason for the former is so compound expressions (and also switch/match) can be cast without parentheses, and also so they can be used as a binary or unary operand. This isn't possible with for/if, since it would create an ambiguity:

for (a) b: c

The cast could apply either to b or to the for expression. The cast could also be substituted with a binary expression, and the problem would be the same.

The point is, assuming we don't change any expression classes here, the parser can't know what kind of expression it should parse until it lexes the first token after the label. This is very similar to the static assert / static let issue documented in #866.

The simplest way to fix this would be to move the label in non-compound expressions:

:label {
    // ...
};

for :label (true) {
    // ...
};

switch :label (0) {
case =>
    // ...
};

I, don't hate this, I think. It's a bit unfortunate I guess, but it doesn't look bad, at least to me. Thoughts?

~ecs 8 months ago

tbh i kinda like just keeping it as for (...) :label { break :label; };, since even though it's semantically a bit weird, it feels much more consistent to the end-user. it also lends itself to an interpretation of switch/match where the {} are a pseudo-compound-expression, which explains the implicit compound-expression in cases. the only changes in this case would be adding switch (...) :label { ... } and the equivalent for match

that being said, for :label (...) { does seem better than :label for (...) {. ig we could also do if :label (...) { ... } else { ... } under that design

~sebsite 8 months ago

I don't really see the status quo as more consistent tbh; syntactically I guess it is, but the semantics are really weird, since you're not really selecting the thing that's being acted on. For expressions are also different in that you break from them, not yield from them. So I think it makes sense that the syntax is slightly different.

I also kinda like switch (...) :label { ... }, for the same reason you described. I'm not sure if I prefer it to switch :label { ... } though... no strong opinion either way I guess. I just want labellable switch/match expressions lol, I'll take anything

Still opposed to adding labels to if expressions, since if expressions don't have any need for it, unlike for/switch/match. When yielding from the body of an if expression, you're yielding from, well, the body, so you should be selecting the compound expression. This is different from breaking from a for expression, since for expressions have their own scope.

~sebsite REPORTED CLOSED 6 months ago

Register here or Log in to comment, or comment via email.