The reasons against having them are complexity and semantics. Also, as Michael Homer's answer contained an option in Java that I've never encountered in my career and I also haven't seen any style guide preventing that, I guess basically nobody even knows that it is allowed. That's a pretty bad trade off for a feature to be added.
Note: A "do continue while false" pattern is essentially what you are looking for. That pattern is very uncommon, but used. Shortening a very uncommon pattern at any cost is uneconomic.
Complexity
Note: this section can be skipped if you assume compilation works by just defining labels and jumping to labels and never optimize or track termination or reachability. Writing code that isn't reachable in the CFG is usually a warning or error to make the programmer aware that he is producing something that cannot even be translated into a connected graph.
Break and continue introduce cross edges into your CFG. The ugly part about them is that they, together with optimizations, can result in arbitrary graph shapes and arbitrary complex path termination even before inline optimization. Without them, one operates on a planar graph that consists of forks and loops which is easy to operate on and also easy to draw for debugging. I.e. debugging the compiler by drawing the result without cross edges almost always results in a layout matching your intuition. With cross edges, that's almost never the case, thus, eating a lot of your time by just trying to understand what's in front of you.
In Tyr, it is allowed to return results from loops, also via break/continue. That adds a bit of extra complexity. E.g. how do you track results precise enough to allow using them directly (example; the names in the function signature get joined correctly and can be used; would not work if you do it in the wrong order, e.g. with labels and replacing labels later).
Additionally, it is allowed to pass blocks bound locally into a function as parameter that is inline called. Allowing break/continue there actually added a lot of complexity, but is also a kind of useful feature to abort Iterator.foreach etc. which are essentially loops if called from a loop (the break/continue would not bind to the loop inside foreach obviously because the caller would not know that it exists). The reason why this adds so much complexity is that the control flow of the inlined function changes while getting inlined and care must be taken not to create disconnected parts.
I wrote an article when working on Tyr 0.7 to deal with the pain it caused me. Most of the break continue tests in the testsuite contains some break/continue tests for Tyr 0.7 (see). With their restricted way, testing them interacting with loops correctly is kind of it. If you would allow them anywhere, you would essentially have to check in arbitrary constellations that reaching the current CFG part's main exit happens and handle cross edges resulting in a lot of extra tests.
Semantics
Is something like
def f = label : {continue label}
a valid function body? In essence it's a self-goto and the same as an empty loop, but would you tell the programmer that it doesn't make sense? How would you detect that? For a syntactic loop statement it is really simple.
An extremely tricky part is finally; while that's already tricky with loops, it gets even more confusing if you just add jumps by allowing break/continue to be used anywhere. To explain why this is tricky, let me make an example:
outer: { val is = new Iterator try { while is.hasNext { try { // do something if(done) return } finally { break /*outer*/ } } } finally { delete is } }
Here, I try to explain several issues in one condensed example.
The first is try finally (and equivalents) is usually used for resource management (delete memory, release connections, close files). In most cases, it is an extremely bad idea to just skip these blocks. I.e. if you exit the function from within the loop , you expect both surrounding finallys getting executed (the return part).
Now, if you allow break/continue/return in a finally block it would get two goto targets in general. Which is an issue, because execution can only continue in one.
For Tyr 0.8, this results in a rule called cross edge suppression. The idea here is to align cross edges with the behavior of exceptions. Exceptions would result in essentially continuing with the next embracing finally block (assuming no catches exist on the path). The same is done with break/continue/return. I.e. the semantics is to try to execute all embracing finally blocks and then complete the initial jump or return.
Finally, an issue is side effects of implicit conversions contributed by control flow joins (mostly phi after loops that return results). If your break/continue with block would allow to return values, you kind of have to keep the entire path structure alive there just to be able to correctly insert such implicit conversions to get the expected semantics (a surrounding finally could have deleted the entity that you need to read in your implicit conversions).
I haven't written an article on that part yet, because I still haven't got all corner cases to work and do, hence, not know if it can work in general.
breakis notgoto. $\endgroup$loop,for,while,if, andbreakis agoto. The language -- and the people who use the language -- just don't want to admit it. (Yes, I'm a greybeard who wrote in line-number BASIC and in assembly.) $\endgroup$let result = …) in the code. $\endgroup$