Skip to content
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

compiler: implement decl literals #21264

Merged
merged 4 commits into from
Sep 1, 2024
Merged

compiler: implement decl literals #21264

merged 4 commits into from
Sep 1, 2024

Conversation

mlugg
Copy link
Member

@mlugg mlugg commented Aug 31, 2024

Pretty straightforward -- see commit messages. I also added "default initialization" decls to ArrayListUnmanaged, HashMapUnmanaged, ArrayHashMapUnmanaged, and GeneralPurposeAllocator.

I am not yet using these in the compiler or standard library to avoid a need for a zig1.wasm update.

Release Notes

Zig 0.14.0 extends the "enum literal" syntax (.foo) to provide a new feature, known as "decl literals". Now, an enum literal .foo doesn't necessarily refer to an enum variant, but, using [Result Location Semantics](langref link), can also refer to any declaration on the target type. For instance, consider the following example:

const S = struct {
    x: u32,
    const default: S = .{ .x = 123 };
};
test "decl literal" {
    const val: S = .default;
    try std.testing.expectEqual(123, val.x);
}
const std = @import("std");

Since the initialization expression of val has a result type of S, the initialization is effectively equivalent to S.default. This can be particularly useful when initializing struct fields to avoid having to specify the type again:

const S = struct {
    x: u32,
    y: u32,
    const default: S = .{ .x = 1, .y = 2 };
    const other: S = .{ .x = 3, .y = 4 };
};
const Wrapper = struct {
    val: S = .default,
};
test "decl literal initializing struct field" {
    const a: Wrapper = .{};
    try std.testing.expectEqual(1, a.val.x);
    try std.testing.expectEqual(2, a.val.y);
    const b: Wrapper = .{ .val = .other };
    try std.testing.expectEqual(3, b.val.x);
    try std.testing.expectEqual(4, b.val.y);
}
const std = @import("std");

It can also help in avoiding [Faulty Default Field Values](langref link), like in the following example:

/// `ptr` points to a `[len]u32`.
pub const BufferA = extern struct {
    ptr: ?[*]u32 = null,
    len: usize = 0
};
// The default values given above are trying to make the buffer default to "empty".
var empty_buf_a: BufferA = .{};
// However, they violate the guidance given in the language reference, because you can write this:
var bad_buf_a: BufferA = .{ .len = 10 };
// That's not safe, because the `null` and `0` defaults are "tied together". Decl literals make it
// convenient to represent this case correctly:

/// `ptr` points to a `[len]u32`.
pub const BufferB = extern struct {
    ptr: ?[*]u32,
    len: usize,
    pub const empty: BufferB = .{ .ptr = null, .len = 0 };
};
// We can still easily create an empty buffer:
var empty_buf_b: BufferB = .empty;
// ...but the language no longer hides incorrect field overrides from us!
// If we want to override a field, we'd have to specify both, making the error obvious:
var bad_buf_b: BufferB = .{ .ptr = null, .len = 10 }; // clearly wrong!

Many existing uses of field default values may be more appropriately handled by a declaration named default or empty or similar, to ensure data invariants are not violated by overriding single fields.

Decl literals also support function calls, like this:

const S = struct {
    x: u32,
    y: u32,
    fn init(val: u32) S {
        return .{ .x = val + 1, .y = val + 2 };
    }
};
test "call decl literal" {
    const a: S = .init(100);
    try std.testing.expectEqual(101, a.val.x);
    try std.testing.expectEqual(102, a.val.y);
}
const std = @import("std");

As before, this syntax can be particularly useful when initializing struct fields. It also supports calling functions which return error unions via try. The following example uses these in combination to initialize a thin wrapper around an ArrayListUnmanaged:

const Buffer = struct {
    data: std.ArrayListUnmanaged(u32),
    fn initCapacity(allocator: std.mem.Allocator, capacity: usize) !Buffer {
        return .{ .data = try .initCapacity(allocator, capacity) };
    }
};
test "initialize Buffer with decl literal" {
    var b: Buffer = try .initCapacity(std.testing.allocator, 5);
    defer b.data.deinit(std.testing.allocator);
    b.data.appendAssumeCapacity(123);
    try std.testing.expectEqual(1, b.data.items.len);
    try std.testing.expectEqual(123, b.data.items[0]);
}

The introduction of decl literals comes with some standard library changes. In particular, unmanaged containers, including ArrayListUnmanaged and HashMapUnmanaged, should no longer be default-initialized with .{}, because the default field values here violate the guidance discussed above. Instead, they should be initialized using their empty declaration, which can be conveniently accessed via decl literals:

const Buffer = struct {
    foo: std.ArrayListUnmanaged(u32) = .empty,
};
test "default initialize Buffer" {
    var b: Buffer = .{};
    defer b.data.deinit(std.testing.allocator);
    b.data.appendAssumeCapacity(123);
    try std.testing.expectEqual(1, b.data.items.len);
    try std.testing.expectEqual(123, b.data.items[0]);
}

Similarly, std.heap.GeneralPurposeAllocator should now be initialized with its .init declaration.

The deprecated default field values for these data structures will be removed in the next release cycle.

@andrewrk andrewrk added the release notes This PR should be mentioned in the release notes. label Aug 31, 2024
@andrewrk
Copy link
Member

Would you mind doing the release notes writeup in the PR description? e.g. https://ziglang.org/download/0.12.0/release-notes.html#Aggregate-Destructuring

@mlugg
Copy link
Member Author

mlugg commented Aug 31, 2024

Done. This may need some rewording if we also rename/change ArrayListUnmanaged etc this release cycle, but otherwise it should be pretty good to go.

@andrewrk
Copy link
Member

Thanks! That helps a lot to reduce release stress :-)

@BratishkaErik
Copy link
Contributor

It can also help in avoiding [Faulty Default Field Values](langref link). For instance, in the above example, it may be incorrect to give the S.x field a default value of 1 in isolation; this may only be correct in combination alongside y = 2.

To be honest I find that langref part and this quote hard to digest, maybe additional existing example would help other people to understand that part. Rephrasing original ifreund comment:

For instance, in the following example, default field values allow initializing struct with an inconsistent state in a way that is not obvious to the reader:

const ArrayListUnmanaged = struct {
    items: []u8 = &.{},
    capacity: usize = 0,
};

// It's not obvious for reader here, that struct has another field `items` initialized with default value, which is now desynced with `capacity`.
const bogus: ArrayListUnmanaged = .{ .capacity = 42 };

// If struct would not have default fields, this code would look a lot more instantly wrong:
const bogus_visible: ArrayListUnmanaged =  .{ .capacity = 42, .items = &.{} }; // something wrong...

Everything else in doc is clear for me. Just some questions, please correct me if I read it wrong:

  • If container is valid with any values of field initializer, it's fine to use .{} initializer and default field values,
  • If there is some combination which can make initialization invalid, and:
    • to correctly initialize container, it's enough to fill instance with pre-made values and put it into default decl (empty if it is collection like ArrayListUnamanged and HashMapUnmanaged?)
    • to correctly initialize container, at least one user value must be passed, it should have init* function(s)?

Rexicon226 and others added 4 commits September 1, 2024 17:34
This is mainly useful in conjunction with Decl Literals (ziglang#9938).

Resolves: ziglang#19777
In favour of newly-added decls, which can be used via decl literals.
@mlugg
Copy link
Member Author

mlugg commented Sep 1, 2024

To be honest I find that langref part and this quote hard to digest

Updated the release notes section to explain via a more concrete example -- yeah, it was a little badly worded.

Just some questions, please correct me if I read it wrong?

That all seems essentially correct. Bear in mind, the names aren't hard rules: empty is a good name where it applies, but default is less good, and I'd generally suggest a more descriptive name than that. In fact, it might make sense to name a default value init; I've done that for GeneralPurposeAllocator here, although I can't justify that decision properly, it just felt right :P

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release notes This PR should be mentioned in the release notes.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants