ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
34.83k stars 2.55k forks source link

Proposal: Make `comptime` struct fields work like `comptime` variables #5675

Open ghost opened 4 years ago

ghost commented 4 years ago

(DRAG IT, DROP IT, FOLD-UNFOLD IT)

5578 proposes, in my opinion, a very ugly solution to its problem. The same use case could be accomplished by accompanying each value with a comptime giveToken, but then there's the problem of encapsulating this functionality. This could be done very cleanly if structs were allowed to exist partly at comptime, partly at runtime, like so:

// **N.B.**: I'm using #1717 syntax because that proposal has been accepted
// -- this proposal does not depend on #1717

const Box = fn (comptime T: type) type {
    return struct {
        const Self = @This();

        val: T,
        comptime times: comptime_int,

        const init = fn (_val: T, comptime _times: comptime_int) Self {\
            return Self { .val = _val, .times = _times };
        };

        // *Self is a hybrid-time pointer, so this function is executed
        // in hybrid time, just like a function with a comptime variable
        const give = fn (self: *Self) T {
            if (self.times <= 0) @compileError("Gave too many times");
            self.times -= 1;

            return self.val;
        };
    };
};

test "Test 1" {
    var runtime_1: usize = 1;
    var a = Box(usize).init(runtime_1, 2);
    _ = a.give();
    _ = a.give();
    _ = a.give(); // should not compile

    // Different instance, different counter
    var b = Box(usize).init(runtime_1, 2);
    _ = b.give();
    _ = b.give();
    _ = b.give(); // should not compile
}

Such fields would of course inherit most of the restrictions of comptime variables -- i.e. no setting in runtime control flow constructs, no setting to runtime values etc., although non-comptime fields would not have these restrictions. Methods would read and write comptime fields but otherwise run at runtime when appropriate. Obviously such a struct cannot be packed or extern, for the same reason that functions with comptime parameters cannot have the C calling convention.

DutchGhost commented 4 years ago

This indeed looks clean, and I think I even tried this as a first attempt to basically have instance bases comptime variables, rather than global ones.

This seems very intuitive and simple to understand to me :)

SpexGuy commented 4 years ago

I'm in agreement on this one. This accomplishes a similar feat to the closure-over-comptime-var trick, but is much easier to read and work with. I'm a bit worried about this leading to potential code bloat or compile time bloat, since the type is effectively generic and every call using it ends up generating a new function. But tuples and closure-over-comptime-var have a similar problem, and people seem to be ok with those, so maybe this is ok too.

SpexGuy commented 4 years ago

Types with comptime fields would have some fundamental limitations, which are essentially the same limitations covering pointers comptime vars. The compiler has to be able to track the instance through its entire lifetime. You can't store it somewhere and retrieve it later, unless the storage location is also a comptime var. Like async, these limitations are viral. A struct containing a struct containing a comptime field inherits all of these limits.

zigazeljko commented 4 years ago

Obviously such a struct cannot be packed or extern.

Why not? The proposed comptime fields would exist only at compile time, so they should not affect runtime memory layout in any way.

ghost commented 4 years ago

5895 encompasses this. Closing.

ghost commented 3 years ago

The crux of #5895 (#7396) is now accepted, so its individual ideas now make sense on their own. Welcome back!

ArborealAnole commented 3 years ago

Types with comptime fields would have some fundamental limitations, which are essentially the same limitations covering pointers comptime vars. The compiler has to be able to track the instance through its entire lifetime. You can't store it somewhere and retrieve it later, unless the storage location is also a comptime var. Like async, these limitations are viral. A struct containing a struct containing a comptime field inherits all of these limits.

What if the user or a struct method specifies the values of the comptime fields when dereferencing pointers to such structs?

rohlem commented 7 months ago

Here's how this proposal could be implemented in my simple mind:

I think equivalent behavior is already doable with @typeInfo and @Type in userspace, but less readable because @Type doesn't support reifying declarations, and it requires manually splitting arguments and routing field accesses, all of which add a lot of noise over the proposed language feature.

TheHonestHare commented 2 months ago

I'd like to point out a completely different usecase that this would also allow: comptime default function parameters.

A somewhat common pattern is to do something like this:

fn foo(args: FooArgs) void {}
const FooArgs = struct {
    a: u32 = 1,
    b: MyEnum = .bar,
    c: u8 = ' ',
};

However, afaik there is no way to do this pattern for comptime arguments. I think this pattern may become ever more common with the addition of decl literals allowing quick shortcuts, and implementing this would allow comptime arguments to participate in this too.

rohlem commented 2 months ago

However, afaik there is no way to do this pattern for comptime arguments.

@TheHonestHare The same is already possible if you separate comptime and runtime arguments into separate types:

fn foo(comptime c: FooComptimeArgs, r: FooRuntimeArgs) void {...}
test foo {foo(.{}, .{});}

Being able to unify both in a single argument can be helpful, though for more complex uses like generic arguments (dependent fields), reflection on an anytype field is still the most versatile. And if every callsite can provide its own type, then status-quo constant comptime fields are often sufficient.