-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Unwrapped optional aliasing footgun #2915
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
Not a bug. After result-location/copy-elision merges
|
I always thought that if (thing.initial_sample) |t| {
// ...
} is shorthand for: if (thing.initial_sample != null) {
const t = thing.initial_sample.?;
// ...
} Is that not the case? If not, it would make sense to update the documentation to call this out explicitly I think. |
Intuitively, I would expect the payload contents to be immutable due to their similarity with function parameters, which have this behavior. |
I think the root cause for confusion is that the syntax for writing to Optionals makes it look like a regular assignment is happening, when it's in fact a write to a pointer location. See the following program, this time the function parameter changes: const std = @import("std");
pub fn noSurprise(x: u32) void {
std.debug.warn("x={}\n", x); // 42
global_x = 420;
std.debug.warn("x={}\n", x); // 42
}
pub fn surprise(x: ?u32) void {
std.debug.warn("x={}\n", x); // 42
global_x_opt = 420;
std.debug.warn("x={}\n", x); // 420 (???)
}
var global_x: u32 = 42;
var global_x_opt: ?u32 = 42;
pub fn main() void {
noSurprise(global_x);
surprise(global_x_opt);
} |
@zimmi how is that really any different from: const std = @import("std");
// just some struct that has more than primitive size
const Foo = struct {
one: u64,
two: [10]u64,
};
var global_foo: Foo = Foo{ .one = 1, .two = undefined };
fn doit(foo: Foo) void {
std.debug.warn("one={}\n", foo.one);
global_foo.one = 99;
std.debug.warn("one={}\n", foo.one);
}
pub fn main() void {
doit(global_foo);
} |
How can Also, in my first example I set the original optional to |
If zig had perfect runtime safety, then this line would crash: global_x_opt = 420; With something like "illegal aliasing detected". There are a couple aliasing issues open to investigate if this safety can be provided. If the current behavior is desired one must make the parameter a const pointer. If a copy is desired one must make a copy explicitly. As is, the code is illegal. |
I was responding to @zimmi , but the same thing applies to the OP. There is a separate thing to consider here which is whether or not an optional can be a type that is always copied for parameters and capture values. A proposal for this would be reasonable. |
@dbandstra, currently:
The reason (which Andrew mentioned is illegal but not yet detected by compiler) setting optional to |
@mikdusan @andrewrk |
In the OP, it might be reasonable for zig to make a copy of the integer. But imagine if it was ?T where T was a very large struct. This is the thing where Zig has "by-value-or-maybe-reference" parameter passing. It might be reasonable to add a semantic rule that, if the payload type of an optional is a primitive, then |
I haven't used Zig for very long and this might be heresy, but: Why not have call by value as the default and require opt-in from the user when call by reference is desired? That's a simpler rule than having to remember the rules for each data type. |
I'd say it's fair to call that the null hypothesis. Zig is still experimenting with this other thing which is meant to improve semantics & performance by having parameter passing make fewer guarantees. Whether or not this experiment is considered a success will be determined by the results of the aliasing research (issues #1108 and #476), how often issues like this one come up, and how much the performance is actually impacted (yet to be measured). Already taken into account is how people will make parameters pointers in C out of performance paranoia. |
I still want to consider this issue but I'm not going to introduce any changes based on it for 0.5.0. |
I ran into the issue of the unwrapped optional getting mutated yesterday and wondered why it crashed: var headOfLinkedList : ?*Node = …;
…
while(headOfLinkedList) |node| {
headOfLinkedList = node.next;
allocator.destroy(node); // this will actually try to destroy the new `headOfLinkedList`
} I think it's better to have |
I ran into this again[1], realizing it only after spending half an hour trying to create a minimal test case thinking my problem was coming from some unrelated compiler bug 🤦♂️ I think, maybe even if it "makes sense" being an alias the way it is now, you have to consider the experience of newbies coming from other languages. I don't know exactly why, but I don't intuitively expect this behaviour[2] - it came as a complete surprise again. If this trips up a lot of people it could harm language adoption. [1] Here. Left side is my workaround after not figuring it out earlier, right side is my fix. I had previously been trying |
This should print 3 both times. Also,
I think this broke some time in the past month. (It manifested as a noticeable runtime bug in my game project.)
The text was updated successfully, but these errors were encountered: