-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
continue expression should have the scope of the while body #1403
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
…>scope` and not just the immediate parent;
My only issue is a symbol appearing before it is defined. I mean, it's not too bad since it should be pretty obvious where it's coming from, but it still feels weird... |
I agree, it feels weird and possibly a bit confusing. Especially with larger loops it might be hard to see where that variable is coming from. Then again I find the whole concept a bit weird, because it's basically a second loop body that appears before the other but gets executed after. |
This doesn't seem weird to me at all and that's why I produced a PR to fix it. |
I think this is a bad idea. Consider this code fn readSections(buffer: []const u8) {
var index: usize = 0;
while (index + header_size <= buffer.len) : (index += section_size) {
const header: *const [header_size]u8 = arrayPointer(buffer, index, header_size);
index += header_size;
if (!hasSectionBody(header)) continue;
const section_size = readInt32(buffer, index);
// some complicated stuff here
if (something) {
somethingElse();
continue;
}
// some other stuff
}
} This has to be a compile error, because the first A better way to structure this is with a labeled break. fn readSections(buffer: []const u8) {
var index: usize = 0;
while (index + header_size <= buffer.len) {
const header: *const [header_size]u8 = arrayPointer(buffer, index, header_size);
index += header_size;
if (!hasSectionBody(header)) continue;
const section_size = readInt32(buffer, index);
continue_: {
// some complicated stuff here
if (something) {
somethingElse();
break :continue_;
}
// some other stuff
}
index += section_size;
}
} |
@thejoshwolfe makes a strong point, and trying to recover the proposal taking this into account starts to get into Too Complicated territory, so I'm going to reject this proposal. |
Re-opening since @kristate made a working PR. There's no explicit argument addressing my point above, but if I may guess, it would be:
I'll make it 0.3.0 since we have a working PR. |
Here are my concerns, why I don't just accept and merge right away:
var i: usize = 0;
while (true) : (i += x) {
if (something) {
const x = 1;
continue;
}
}
|
If I may add my two cents, I find the
we do see function usage before function definition all the time though so maybe this is analogous |
@andrewrk I want this feature and I think it is intuitive to the language. My most simple response to all of this is that you mentioned in your original post:
I agree with this. This should be a thing and it can be!
They already know how they would work -- and if they are wrong, the compiler will tell them. No offense to @thejoshwolfe , they will (and I have) spent in the past 10 minutes trying to understand the kind of break/continue mechanics in his post above.
It shouldn't and doesn't compile with the patch:
Cross posing from the PR, but in the event that unreachable code is referenced, we get an error message. @thejoshwolfe did a good job working through this and I thank him for pointing it out.
where the following code was used to test: test "reference variable from unreachable code region" {
var i: usize = 0;
while (i <= 10) : ({ i += b; }) {
continue;
const b = 42;
}
} |
…>scope` and not just the immediate parent;
… from unreachable code region';
(edit2: I realized some error in my understanding/reasoning. See 2 comments down.) To productively add to the discussion, I just thought of a counter-proposal not yet on the table: Introducing a new kind of "continue defer", specifically for
|
re /me - the last message makes some valid analogies but maybe doesn't quite hit the point of this thread. So to sum it up more usefully (and quite opinionated):
But then again, that's kind of the whole point of this thread. So maybe I'm still missing the point a bit. |
I just realized my mistake. |
@rohlem Thanks for putting in the thought. from my perspective, continue expression should share the same scope as the |
Update: I've now also realized the difference in behaviour between continue expressions and
vs
In many cases the difference will be negligible, but it can lead to subtle mistakes (the worst kind of mistakes). So normal |
I don't think "scope" is very well defined here. Back when we had As far as i know, variable declarations still create scopes in the compiler, which means the "while body scope" is actually not well defined. The while body is a series of nesting scopes corresponding to each variable declaration. If you define the continue expression to exist in the last scope, it would forbid you from doing any I haven't studied your code, but you must have put in a check for referencing a variable without properly declaring it. That's not how local variable references work elsewhere in the compiler, which suggests that this is a bad idea. If we run with this proposal, we're going to need an answer for this question: can you jump to a continue expression before a variable is declared as long as the variable is not used in the continue expression as determined by comptime analysis? Here's an example: while (foo()) : (if (comptime is_windows) print(handle)) {
if (conptime !is_windows) continue;
var handle = bar();
} Writing a specification for the language's behavior in this case would not be pleasant. This feature seems too weird. |
if I guess I need to know what variables need to or are expected to be set. const builtin = @import("builtin");
const std = @import("std");
const is_windows = builtin.os == builtin.Os.windows;
fn foo() bool {
return false;
}
fn bar() u32 {
return 42;
}
test "josh" {
while (foo()) : (if (comptime is_windows) std.debug.warn("{}\n", handle)) {
if (comptime !is_windows) continue;
var handle = bar();
}
} |
Here is another test: const builtin = @import("builtin");
const std = @import("std");
const is_windows = false; //builtin.os == builtin.Os.windows;
fn foo(i: usize) bool {
return i < 10;
}
fn bar() u32 {
return 42;
}
test "josh" {
var i: usize = 0;
while (foo(i)) : (if (comptime is_windows) std.debug.warn("{}\n", handle)) {
i += 1;
if (comptime !is_windows) continue;
var handle = bar();
}
} returns
const builtin = @import("builtin");
const std = @import("std");
const is_windows = true; //builtin.os == builtin.Os.windows;
fn foo(i: usize) bool {
return i < 10;
}
fn bar() u32 {
return 42;
}
test "josh" {
var i: usize = 0;
while (foo(i)) : (if (comptime is_windows) std.debug.warn("{}\n", handle)) {
i += 1;
if (comptime !is_windows) continue;
var handle = bar();
}
} returns
|
Here is an example of surprising code snippets if we accept this proposal: test "while cont" {
var i: usize = 0;
while (i < 10) : (i += x) {
if (true) {
const x = 1;
continue;
}
}
}
Meanwhile these both work: test "while cont" {
var i: usize = 0;
while (i < 10) : (i += x) {
const x = 1;
continue;
}
}
test "while cont 2" {
var i: usize = 0;
while (i < 10) : (i += x) {
{
const x = 1;
continue;
}
}
} And here's an example of how the proposed implementation is unsound: test "while cont" {
var i: usize = 0;
while (i < 10) : (i += x) {
var runtime_true = true;
if (runtime_true) {
continue;
}
var x: u32 = 1;
}
} With the proposed implementation, this test case uses the value Now I want to point something important out. I didn't know about this code example until I fortunately thought of it right now. But I sensed that something was amiss early on:
In a world of Unknown Unknowns, Zig's saving grace is resisting the urge to make things more complicated, and keeping things very simple, even if it's inconvenient sometimes. I think this sense of avoiding things that start to get Too Complicated is really important, and possibly the main attraction of Zig. I know it's been frustrating for you, since you've made multiple pull requests and had them rejected. But we absolutely must keep the language simple. |
Without this proposal, it's possible to define the semantics of the continue expression through syntactic desugaring. In general, the following is always true, where while (COND) : (EXPR) {
BODY
}
// is equivalent to:
while (true) {
if (!(COND)) break;
body: {
BODY // and any `continue` in BODY is replaced by `break :body`
}
EXPR
}
// or with only if and goto:
entry:
{
if (!(COND)) {
goto :break_;
}
{
BODY // and replace `continue` with `goto :continue_`
}
continue_:
EXPR
goto :entry;
}
break_: ( The proposal in this issue makes this desugaring not work. I'm not sure it would even be possible to define the continue expression with desugaring. You can't take away the I'm not sure this helps explain my objection to the semantics proposed here. It's good for language design when high-level control flow constructs are precisely definable in low-level terms. |
|
|
I also moved `ncmd` into a shorter scope.
I also moved `ncmd` into a shorter scope.
I also moved `ncmd` into a shorter scope.
Here's a use case:
This code doesn't work, but it should:
This is after fixing a bug where I forgot the
ptr += ...
part, and ability to put it in the continue expression would have prevented the bug.The text was updated successfully, but these errors were encountered: