Background
In Zig, fields on struct and tuple types can be marked as comptime. Such fields must specify a default field value. When comptime fields are loaded, they always contain the comptime-known field value specified at the type declaration site. These fields do not appear in the runtime memory layout of the type, lack meaningful addresses, and cannot vary between different values of the same type. In many ways, they are more akin to declarations than to fields.
Rationale
There are two main motivations for comptime fields existing in Zig:
-
When using @call, it may be necessary to specify comptime-known values for comptime parameters, but runtime-known values for other parameters. comptime tuple fields allow for this: the tuple fields corresponding to comptime parameters must be comptime fields, but the other fields can be normal fields, and a runtime-known value of this tuple type can be passed to @call.
-
It allows functions which accept their own argument tuples, like std.debug.print (and other formatted printing functions), to accept a mix of comptime-only values and runtime-known values, e.g: std.debug.print("runtime value {d}, comptime-only {any}\n", .{ runtime_int, u32 });
However, in exchange for these benefits, comptime fields act as a major point of complication and confusion in the language. Almost as a rule, when a beginner to Zig discovers the existence of comptime fields, they assume that they can be mutated at compile time, because the actual behavior of comptime fields is very unintuitive. (Of course, allowing these fields to be mutated at comptime has been proposed in the past; but unfortunately such a change would introduce an unacceptable amount of language complexity.) In terms of the langauge specification, because comptime fields are not associated with any particular instance of a struct/tuple in memory, they act as awkward exceptions to the usual type layout rules (for instance aligning these fields does not affect the natural alignment of the struct type containing them), and pointers to these fields act unusually (almost like pointers to const declarations, but not quite), such as in their lack of support for @fieldParentPtr (because the field pointer is not associated to any specific struct instance).
This doesn't really matter to the proposal, but it is worth quickly noting that the justifications given above for the existence of comptime fields only really apply to tuple fields: @call only accepts tuples, and while our formatted printing functions can accept struct types, that functionality is used somewhat rarely.
Proposal
Remove comptime fields from Zig. This renders a very small handful of operations impossible, in exchange for a simplified grammar, a significantly simplified compiler implementation and language specification, and the elimination of a frequent point of confusion for beginners.
Note that because only comptime fields can have default values in tuple types (this is already the case), this proposal completely eliminates default values from tuple types.
Below is some brief discussion of the two affected use cases (the ones I listed above as motivations for the existence of comptime fields).
This proposal makes it impossible to directly use @call at runtime on functions with comptime parameters:
fnaddComptimeRuntime(comptimea:u32,b:u32)u32{returna+b;}fnadd10(x:u32)u32{constlhs=10;// Assume that `@call` is necessary here. For instance, perhaps a specific modifier is needed.// This call works today, but would be a compile error under this proposal, because the first// argument must be comptime-known while the second argument is runtime-known.return@call(.auto,addComptimeRuntime,.{lhs,x});}pubfnmain()void{_=add10(100);}
However, it is still possible to create a wrapper function which captures your comptime parameter values in closure, so add10 could be rewritten like this:
fnadd10(x:u32)u32{constlhs=10;conststatic=struct{fnadd10Inner(x:u32)u32{returnaddComptimeRuntime(lhs,x);}};return@call(.auto,static.add10Inner,.{x});}
To be clear, I am in no way claiming that this is better than status quo: I am merely pointing out that the use case can still be satisfied, albeit in a more awkward way. I don't think this is a real problem for what is a quite rare use case.
It is also worth noting that the main use case for @call in practice is functions like std.Io.async which accept, as arguments, a function and a tuple of arguments to pass to it with @call. This use case already tends to fail for comptime parameters today due to the (very reasonble!) use of std.meta.ArgsTuple, which gives a tuple type containing no comptime fields. That is, the following snippet will emit a compile error:
fnaddComptimeRuntime(comptimea:u32,b:u32)u32{returna+b;}pubfnmain(init:std.process.Init)void{constlhs=10;constrhs=10;varfuture=init.io.async(addComptimeRuntime,.{lhs,rhs});_=future.await(init.io);}conststd=@import("std");
(...okay, well, apparently right now it actually crashes the compiler! Sorry about that---although I think the fact that not many people have hit this bug reinforces the argument that this is not a common use case.)
So to make that work, you already need to use comptime closure (as discussed above), like this:
fnaddComptimeRuntime(comptimea:u32,b:u32)u32{returna+b;}pubfnmain(init:std.process.Init)void{constlhs=10;constrhs=20;conststatic=struct{fninner()u32{returnaddComptimeRuntime(lhs,rhs);}};varfuture=init.io.async(static.inner,.{});_=future.await(init.io);}conststd=@import("std");
So, since this use case is very rare, can still be served (albeit in a more awkward manner), and the pattern being broken already frequently fails today, I do not think this is a serious concern.
This proposal makes it impossible to call formatted printing functions like std.debug.print on mixed comptime-only values and runtime-known values, such as the following:
pubfnmain()void{foo(123);}fnfoo(x:u32)void{std.debug.print("value {d} of type {any}\n",.{x,@TypeOf(x)});}conststd=@import("std");
(This is also hit if you use an uncoerced numeric literal as an argument to formatted printing, because comptime_int and comptime_float are comptime-only types.)
I don't have much to say about this one: I simply do not believe this use case is common or valuable enough to be worth the language complexity it invokes. The only case where I think someone would realistically be hindered by this is initial experiementation while learning Zig (for instance, writing a numeric literal or a type as a print argument when first learning the formatted printing API). Don't get me wrong, that is a valuable use case!--but I don't think it can justify the complexity of this language feature.
## Background
In Zig, fields on struct and tuple types can be marked as `comptime`. Such fields *must* specify a default field value. When `comptime` fields are loaded, they always contain the comptime-known field value specified at the type declaration site. These fields do *not* appear in the runtime memory layout of the type, lack meaningful addresses, and cannot vary between different values of the same type. In many ways, they are more akin to declarations than to fields.
## Rationale
There are two main motivations for `comptime` fields existing in Zig:
* When using `@call`, it may be necessary to specify comptime-known values for `comptime` parameters, but runtime-known values for other parameters. `comptime` tuple fields allow for this: the tuple fields corresponding to `comptime` parameters must be `comptime` fields, but the other fields can be normal fields, and a runtime-known value of this tuple type can be passed to `@call`.
* It allows functions which accept their own argument tuples, like `std.debug.print` (and other formatted printing functions), to accept a mix of comptime-only values and runtime-known values, e.g: `std.debug.print("runtime value {d}, comptime-only {any}\n", .{ runtime_int, u32 });`
However, in exchange for these benefits, `comptime` fields act as a major point of complication and confusion in the language. Almost as a rule, when a beginner to Zig discovers the existence of `comptime` fields, they assume that they can be *mutated* at compile time, because the actual behavior of `comptime` fields is very unintuitive. (Of course, allowing these fields to be mutated at comptime has been proposed in the past; but unfortunately such a change would introduce an unacceptable amount of language complexity.) In terms of the langauge specification, because `comptime` fields are not associated with any particular instance of a struct/tuple in memory, they act as awkward exceptions to the usual type layout rules (for instance aligning these fields does not affect the natural alignment of the `struct` type containing them), and pointers to these fields act unusually (*almost* like pointers to `const` declarations, but not quite), such as in their lack of support for `@fieldParentPtr` (because the field pointer is not associated to any specific struct instance).
This doesn't really matter to the proposal, but it is worth quickly noting that the justifications given above for the existence of `comptime` fields only really apply to *tuple* fields: `@call` only accepts tuples, and while our formatted printing functions *can* accept struct types, that functionality is used somewhat rarely.
## Proposal
Remove `comptime` fields from Zig. This renders a very small handful of operations impossible, in exchange for a simplified grammar, a significantly simplified compiler implementation and language specification, and the elimination of a frequent point of confusion for beginners.
Note that because only `comptime` fields can have default values in tuple types (this is already the case), this proposal completely eliminates default values from tuple types.
Below is some brief discussion of the two affected use cases (the ones I listed above as motivations for the existence of `comptime` fields).
---
This proposal makes it impossible to directly use `@call` at runtime on functions with `comptime` parameters:
```zig
fn addComptimeRuntime(comptime a: u32, b: u32) u32 {
return a + b;
}
fn add10(x: u32) u32 {
const lhs = 10;
// Assume that `@call` is necessary here. For instance, perhaps a specific modifier is needed.
// This call works today, but would be a compile error under this proposal, because the first
// argument must be comptime-known while the second argument is runtime-known.
return @call(.auto, addComptimeRuntime, .{ lhs, x });
}
pub fn main() void {
_ = add10(100);
}
```
However, it is still possible to create a wrapper function which captures your `comptime` parameter values in closure, so `add10` could be rewritten like this:
```zig
fn add10(x: u32) u32 {
const lhs = 10;
const static = struct {
fn add10Inner(x: u32) u32 {
return addComptimeRuntime(lhs, x);
}
};
return @call(.auto, static.add10Inner, .{x});
}
```
To be clear, I am in no way claiming that this is better than status quo: I am merely pointing out that the use case can still be satisfied, albeit in a more awkward way. I don't think this is a real problem for what is a quite rare use case.
It is also worth noting that the main use case for `@call` in practice is functions like `std.Io.async` which accept, as arguments, a function and a tuple of arguments to pass to it with `@call`. This use case already tends to fail for `comptime` parameters *today* due to the (very reasonble!) use of `std.meta.ArgsTuple`, which gives a tuple type containing no `comptime` fields. That is, the following snippet will emit a compile error:
```zig
fn addComptimeRuntime(comptime a: u32, b: u32) u32 {
return a + b;
}
pub fn main(init: std.process.Init) void {
const lhs = 10;
const rhs = 10;
var future = init.io.async(addComptimeRuntime, .{ lhs, rhs });
_ = future.await(init.io);
}
const std = @import("std");
```
(...okay, well, apparently right now it actually crashes the compiler! Sorry about that---although I think the fact that not many people have hit this bug reinforces the argument that this is not a common use case.)
So to make that work, you *already* need to use comptime closure (as discussed above), like this:
```zig
fn addComptimeRuntime(comptime a: u32, b: u32) u32 {
return a + b;
}
pub fn main(init: std.process.Init) void {
const lhs = 10;
const rhs = 20;
const static = struct {
fn inner() u32 {
return addComptimeRuntime(lhs, rhs);
}
};
var future = init.io.async(static.inner, .{});
_ = future.await(init.io);
}
const std = @import("std");
```
So, since this use case is very rare, *can* still be served (albeit in a more awkward manner), and the pattern being broken already frequently fails today, I do not think this is a serious concern.
---
This proposal makes it impossible to call formatted printing functions like `std.debug.print` on mixed comptime-only values and runtime-known values, such as the following:
```zig
pub fn main() void {
foo(123);
}
fn foo(x: u32) void {
std.debug.print("value {d} of type {any}\n", .{ x, @TypeOf(x) });
}
const std = @import("std");
```
(This is also hit if you use an uncoerced numeric literal as an argument to formatted printing, because `comptime_int` and `comptime_float` are comptime-only types.)
I don't have much to say about this one: I simply do not believe this use case is common or valuable enough to be worth the language complexity it invokes. The only case where I think someone would realistically be hindered by this is initial experiementation while learning Zig (for instance, writing a numeric literal or a type as a `print` argument when first learning the formatted printing API). Don't get me wrong, that is a valuable use case!--but I don't think it can justify the complexity of this language feature.