C2 forum
General Category => Ideas => Topic started by: lerno on November 28, 2018, 12:27:25 PM
-
Putting it here instead as a comment in a longer discussion
Here is the propsal:
switch (a) {
case 1 | // Using : instead of | is syntax sugar for case 1: goto next;
case 2 |
case 3:
foo(); // With : we get a break;
case 4 |
case 5:
bar();
goto next; // If we want to execute a statement then pass to next.
case 6:
baz();
}
// The above in C:
switch (a) {
case 1:
case 2:
case 3:
foo();
break;
case 4:
case 5:
bar();
case 6:
baz();
break;
}
Another possibility is |: or :|
switch (foo) {
case 1|:
case 2|:
case 3:
do_something();
Using | means a that some care must be taken if there is an expression of type:
case 1|2: (This is is compiled to case 3: or course, but I think it's reasonable to require ( ) on any constant expression that isn't a literal.
So case 1+2: would not be allowed, but case (1+2): is ok.
Alternatives to goto next would be:
case 4:
foo();
fallthrough;
case 5:
...
case 4:
foo();
goto case 5;
case 5:
...
case 4:
foo();
goto case;
case 5:
...
case 4:
foo();
continue case;
case 5:
...
case 4:
foo();
next;
case 5:
...
case 4:
foo();
next case;
case 5:
...
case 4:
foo();
nextcase;
case 5:
...
An even more lightweight syntax uses | for fallthrough, leading to this uniform syntax look:
case 4:
foo();
| case 5:
...
switch (a) {
case 1:
| case 2:
| case 3:
foo(); // implicit break here
case 4 :
| case 5:
bar(); // Fallthrough here because the next starts with |
| case 6:
baz();
}
-
Real life example of this kind of switch:
switch (D->getKind()) {
case DECL_FUNC:
expr->setConstant();
case DECL_VAR:
VarDecl* VD = cast<VarDecl>(D);
QualType T = VD->getType();
if (T.isConstQualified()) {
Expr* Init = VD->getInitValue();
if (Init) {
// Copy CTC status of Init Expr
expr->setCTC(Init->getCTC());
}
expr->setConstant();
return;
}
case DECL_ENUMVALUE:
expr->setCTC(CTC_FULL);
expr->setConstant();
return;
case DECL_ALIASTYPE |
case DECL_STRUCTTYPE |
case DECL_ENUMTYPE |
case DECL_FUNCTIONTYPE |
expr->setConstant();
case DECL_ARRAYVALUE |
case DECL_IMPORT |
case DECL_LABEL:
}
-
And the uniform | syntax instead:
Real life example of this kind of switch:
switch (D->getKind()) {
case DECL_FUNC:
expr->setConstant();
case DECL_VAR:
VarDecl* VD = cast<VarDecl>(D);
QualType T = VD->getType();
if (T.isConstQualified()) {
Expr* Init = VD->getInitValue();
if (Init) {
// Copy CTC status of Init Expr
expr->setCTC(Init->getCTC());
}
expr->setConstant();
return;
}
case DECL_ENUMVALUE:
expr->setCTC(CTC_FULL);
expr->setConstant();
return;
case DECL_ALIASTYPE:
| case DECL_STRUCTTYPE:
| case DECL_ENUMTYPE:
| case DECL_FUNCTIONTYPE:
expr->setConstant();
case DECL_ARRAYVALUE:
| case DECL_IMPORT:
| case DECL_LABEL:
// do nothing.
}
-
Also to discuss, should we automatically open a scope or not. If we automatically open a scope in cases, we prevent this nice code:
switch (state) {
case READ_WITH_LOCK:;
LOCK *lock = acquire_lock();
defer lock.release();
case READ_WITHOUT_LOCK:
read_from_resource();
break;
case ...
}
This since the implicit scope would trigger the defer when passing into the READ_WITHOUT_LOCK (I assume current syntax, so this is fallthrough automatically)
Of course, we could make | / fallthrough mean that scope is shared. That would make this execute correctly (new syntax):
switch (state) {
case READ_WITH_LOCK:;
LOCK *lock = acquire_lock();
defer lock.release();
| case READ_WITHOUT_LOCK:
read_from_resource();
case ...
}
-
Yet one more syntax:
switch (a) {
case 1, // Comma to delimit multiple case. Can only be followed by another case
case 2,
case 3: // : means we have an explicit break.
foo();
case 4,
case 5:
bar();
goto next; // Explicit fallthrough (could also be goto case, goto case 6 etc)
case 6:
baz();
}
-
So, current options:
(A) case 3 | for empty fallthrough, explicit fallthrough when it is non empty.
(B) | case 3 to indicate the previous case was fallthrough, regardless whether it was empty or not.
(C) case 3, for empty fallthrough, explicit fallthrough when it is non empty.
Types of "fallthrough" keyword:
(a) fallthrough
(b) goto next
(c) goto case
(d) goto case 4
(e) continue case
(f) next
(g) nextcase
-
Wow, that's a lot of posts in one day..
The case of case 3|
looks quite nice, but it differs from the case where you have some code, but also a fall-through.
I want to simplify the language as much as possible, so I don't really want to change the current behavior too much. If you think
about the possible bugs that often occur is only that a develop forgets to add the break keyword. So the only needed option I
think is to add the fallthrough keyword and require that for all fallthroughs. So for example:
case 1:
break;
case 2:
fallthrough;
case 3:
fallthrough:
default:
// ...
or maybe even allow empty cases to fallthrough without keyword to make code with lots of fallthrough's better readable
case 1:
break;
case 2: // allowed because empty case
case 3:
fallthrough;
default:
// ..
-
I think fallthrough automatically on empty is very inconsistent. Consider this, with impllcit break:
func void printTest(i32 foo) {
switch (foo) {
case 1:
printf("A\n");
case 2:
printf("B\n");
default:
// Do nothing
}
}
printTest(1);
Prints "A"
Now we decide to comment out printf("A\n");
func void printTest(i32 foo) {
switch (foo) {
case 1:
// printf("A\n");
case 2:
printf("B\n");
default:
// Do nothing
}
}
printTest(1);
Now this prints "B". Perhaps not what we expected... So I think case 1, is a better suggestion for empty fallthrough (version C)
-
Fallthrough is always automatic, except when you use break. So there is never an automatic break.
The compiler gives gives a warning/error if you don't use break/fallthrough in cases there is a code..
-
Err... My proposal was to add implicit break.
-
Ahh missed that sorry
I think the common case for a switch is that every case breaks. Fallthrough is a much rarer case. So I think making
all cases break by default is an improvement indeed. Syntax wise, this means the following:
- keyword break is allowed in a case.
- keyword fallthrough is used to indicate that the case will fallthrough (there must be a next case).
I'll put in on the roadmap
-
I was thinking about the fallthrough keyword and came up with a situation that requires some thinking.
How would we handle:
...
case A:
if (b == 10) fallthrough;
do_something();
case B:
..
So the fallthrough can be used to 'jump' to the next case. Or only allow fallthrough at the end?
To avoid this complexity, I see that Go (golang.org) only allows fallthrough at the end. So probably that is best..
-
Well, if you look at the alternatives for fallthrough:
(a) fallthrough
(b) goto next
(c) goto case
(d) goto case 4
(e) continue case
(f) next
(g) nextcase
Here obviously b, c, d, e, g all clearly indicate that there's a jump. (d) is even more explicit of course, not to mention flexible.