-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Proposal: allow structs and unions to be const, similar to pointers #18964
Comments
|
Yes. If this is the reason to reject the proposal, I find it reasonable.
Yes, but there is a difference between a "const variable" (I called it const identifier) and a "const type". const std = @import("std");
const S = struct { ptr: *u8 };
fn inc(s: S) void {
s.ptr.* += 1;
}
test {
var num = @as(u8, 10);
const s = S{ .ptr = &num };
inc(s);
try std.testing.expectEqual(@as(u8, 11), num);
} I don't understand why this point is related to the proposal.
I did not understand this. The following code does not compile because test {
const S = struct { ptr: *u8 };
const num = @as(u8, 10);
const s = S{ .ptr = &num };
_ = s;
} Error: a.zig:4:25: error: expected type '*u8', found '*const u8'
const s = S{ .ptr = &num };
^~~~
a.zig:4:25: note: cast discards const qualifier
Thanks, I did not know about it. They are related but (in my view) different. That one is about restricting the permission to write some fields in a struct out side of a namespace (focusing on encapsulation). This one has the focus on marking a function argument or identifier/variable in a block as "write-through-pointers free". |
they do. that code is attempting to do the opposite as shown by the compile error. |
I think I understand the use case for making a type's pointers transitively pointing-to- untested example code of how I would write it (EDIT: added .Optional, .ErrorUnion, fixed .Pointer type memoization)
const std = @import("std");
/// helper function for deduplicating created optional types via comptime memoization
fn TransitivelyPointingToConstOptional(comptime optional_info: std.builtin.Type.Optional, comptime Child: type) {
var o = optional_info;
o.child = Child;
return @Type(.{.Optional = o});
}
/// helper function for deduplicating created error union types via comptime memoization
fn TransitivelyPointingToConstErrorUnion(comptime error_union_info: std.builtin.Type.ErrorUnion, comptime Payload: type) {
var e = error_union_info;
e.payload = Payload;
return @Type(.{.ErrorUnion = e});
}
/// helper function for deduplicating created pointer types via comptime memoization
fn TransitivelyPointingToConstPointer(comptime pointer_info: std.builtin.Type.Pointer, comptime Child: type) {
var p = pointer_info;
p.child = Child;
return @Type(.{.Pointer = p});
}
/// helper function for deduplicating created struct types via comptime memoization
fn TransitivelyPointingToConstStruct(comptime struct_info: std.builtin.Type.Struct, comptime field_count: comptime_int, comptime fields: [field_count]std.builtin.Type.StructField) {
var s = struct_info;
s.fields = &fields;
return @Type(.{.Struct = s});
}
/// helper function for deduplicate created union types via comptime memoization
fn TransitivelyPointingToConstUnion(comptime union_info: std.builtin.Type.Union, comptime field_count: comptime_int, comptime fields: [field_count]std.builtin.Type.UnionField) {
var s = union_info;
s.fields = &fields;
return @Type(.{.Union = u});
}
pub fn TransitivelyPointingToConst(comptime T: type) type {
return switch(@typeInfo(T)) {
else => T,
.Optional => |O| {
var o = O;
const Child = TransitivelyPointingToConst(o.child);
o.child = undefined;
return TransitivelyPointingToConstOptional(o, Child);
},
.ErrorUnion => |E| {
var e = E;
const Payload = TransitivelyPointingToConst(e.payload);
e.payload = undefined;
return TransitivelyPointingToConstErrorUnion(e, Payload);
},
.Pointer => |P| {
var p = P;
p.is_const = true;
const Child = TransitivelyPointingToConst(p.child);
p.child = undefined;
return TransitivelyPointingToConstPointer(p, Child);
},
.Struct => |S| {
var s = S;
const fields_len = s.fields.len;
var fields = s.fields[0..].*;
s.fields = undefined;
for (&fields) |*f| {
f.type = TransitivelyPointingToConst(f.type);
}
return TransitivelyPointingToConstStruct(s, fields_len, fields);
},
.Union => |U| {
var u = U;
const fields_len = u.fields.len;
var fields = u.fields[0..].*;
u.fields = undefined;
for (&fields) |*f| {
f.type = TransitivelyPointingToConst(f.type);
}
return TransitivelyPointingToConstUnion(u, fields_len, fields);
},
};
}
pub fn asTransitivelyPointingToConst(value: anytype) TransitivelyPointingToConst(@TypeOf(value)) {
return @bitCast(value);
}
// /// the other direction would look like this, but is generally unsafe,
// /// because it makes pointers that allow mutating what might be const
//pub fn fromTransitivelyPointingToConst(comptime Target: type, value: TransitivelyPointingToConst(Target)) Target {
// return @bitCast(value);
//} Have you considered/tried such a userspace solution? Having a language feature would of course further reduce friction, which I'm not opposed to. |
Sorry, I swapped stuff.
|
Yes. Your implementation looks the same as mine (I said it was a hack, and called One of the reasons I called it a hack is that I have no guarantees that the
I think that is all. As time goes on (and more answers come) I'm considering that putting it somewhere in
I think this is the main (maybe only) good reason to make it a language feature, instead of a user-level comptime magic.
Makes sense.
This is a point for keeping it at user level.
An easy change is to move all the pointer modifiers to before
I'm not sure if I like it. Maybe I just got used to |
Oh, sorry, not sure how I missed that in the OP.
You're right, while I'm pretty sure the compiler currently has no reason to make the layouts incompatible,
Type operators so far are prefix operators, meaning they are right-associative and read left-to-right. |
Agree, test "Const idempodent" {
try expectEqual(Const(*u8), Const(Const(*u8)));
try expectEqual(Const([]u8), Const(Const([]u8)));
try expectEqual(Const([*]u8), Const(Const([*]u8)));
try expectEqual(Const([3]*u8), Const(Const([3]*u8)));
try expectEqual(Const(?*u8), Const(Const(?*u8)));
try expectEqual(Const(????*u8), Const(Const(????*u8)));
try expectEqual(Const(error{A}!*u8), Const(Const(error{A}!*u8)));
try expectEqual(Const(error{A}!?*u8), Const(Const(error{A}!?*u8)));
try expectEqualTypes(Const(Struct), Const(Const(Struct))); // My definition for equal types is fine
try expectEqual(Const(Struct), Const(Const(Struct))); // This line fails
} It would be nice to solve it, maybe it requires #18816 .
Similarly, I think that the language does not guarantees that type-punning works.
Agree. My made-up syntax is bad. |
@Kiyoshi364 The missing element for idempotency was only re-creating New version with passing tests: const.zig.txt EDIT: Looking at this again, maybe the separate helper functions aren't even necessary / what you want anymore, Test case for distinct identities (EDIT3: Updated to include fields):
const expect = std.testing.expect;
test "Const identity" {
const A = struct{a: *u8};
const B = struct{a: *u8};
const C = union{a: *u8};
const D = union{a: *u8};
try expect(A != B);
try expect(Const(A) != Const(B));
try expect(C != D);
try expect(Const(C) != Const(D));
const E = struct{a: *A};
const F = struct{a: *B};
const G = union{a: *C};
const H = union{a: *D};
try expect(E != F);
try expect(Const(E) != Const(F));
try expect(G != H);
try expect(Const(G) != Const(H));
} |
I inlined the helper functions, and it worked. I have updated with all of that: const.zig.txt I'm happy with the current state of I'm closing the issue. |
PS: Before explaining the proposal, I'm a bit confused. When creating a new issue, I get to choose which kind of issue it is. The choice for "Language Proposal", points to here, saying that ziglang is not accepting new language proposals. But there are a few recent issues tagged with "Proposal".
I wrote the entire proposal before finding this out, so I may just leave it here, right? If ziglang is really not accepting new language proposals, just ignore it.
What would mean a struct/union to be const?
At first, it means that the struct's/union's pointer field's are const.
In the following example, the
const Struct
would mean the same as theConstStruct
.A hack around it is to use
*const StructOrUnion
in place ofconst StructOrUnion
in function argument lists, ie:fn foo(a: *const StructOrUnion) void
instead offn foo(a: const StructOrUnion) void
.This hack is not that good for 2 reasons. First, it requires to add a
&
at every call site (it is just annoying, but fine). And second, (please correct me if I'm wrong) it locks the compiler into actually passing a pointer to the function in the compiled code, instead of choosing the best between passing via pointer or via value (passing via value is good for small structs/unions).Here is another hack. It can also be used to explain the semantics of the proposal. The only drawback of this hack that I can think of is the extra verbosity of to_const. The compiler could have infer it (and maybe some runtime cost of shuffling data around, the compiler may be smart enough to avoid this).
The implementation (with some tests) for the hack is in const.zig.txt.
For the compiler/type-checking, I believe that when accessing a struct, union or pointer field, the fields type should also be const. I may be wrong about it, I don't fully understand all the type inference rules and stuff.
Why I think it is a good idea
The use case I first think of is when writing a function which receives a struct/union argument with one or more pointer fields. If I (as function writer) want to guarantee that it does not write no any of the argument pointer fields.
The idea is "an extension" of receiving a
*const T
instead of*T
. A note/remainder: arguments to functions are always semantically passed by value and they are "const identifiers" (see "## const/var identifier vs const/non-const type" bellow).An immediate example is query functions (look at this struct/union and get some information out). Here is a mock example of a generic game board. The board uses a pointer to store its tiles. There are some functions that writes to the slice (move_piece) and other functions that don't (is_tile_blue).
Extra thoughts
const/var identifier vs const/not-const type
Most likely I'm just making the name of it up. This subsection is to remark the existence of 2 different uses of the const keyword.
An identifier (people might call it "variable") can be declared with either
const
orvar
. As far as I know, the only difference is that withconst
the programmer cannot mutate the underlying value (the value which the identifier identifies or refers/"points" to). While withvar
the programmer can mutate the underlying value.In the type level/system, a pointer to
T
can beconst
(e.g.*const T
) or not-const
(e.g.*T
). If it is const, the programmer cannot write to it.This allow one to declare a pointer identifier as
const
and also change the value that the pointer points to:If this is confusing the article may help:
Pointers and constness in Zig and why it is confusing to a C programmer
Type inference (Peer Type Resolution?)
The type
*T
can implicitly cast into*const T
.Similary,
StructOrUnion
should implicitly cast intoconst StructOrUnion
.var identifier of type const StructOrUnion
A
var
identifier (a variable) of typeconst StructOrUnion
should behave similarly to avar
identifier of type*const T
: the programmer can alter the identifier's underlying value, but cannot write anything after a dereference.The zig grammar
Currently,
const
can only appear in type expressions after*
(e.g.* const u8
) (I didn't look at the grammar specs, I tryed and the compiler complained). As a consequence,const struct{}
andconst u8
are invalid type expressions.I didn't look at the grammar (as stated previously), so I don't know how hard it is to allow
const struct{}
and similar expressions.Assuming that it is possible, identifiers also should be allowed (e.g.
const Board
). If it is allowed, I expect that the grammar would also allow primitive types (e.g.const u8
) and multiple-consts (e.gconst const Board
). If these cases are undesirable, the compiler should make extra semantic checks (if one asks my opinion: it is undesirable).The text was updated successfully, but these errors were encountered: