Need help replacing BoundedArray

I’ve been trying to replace all of my instances of BoundedArray with ArrayListUnmanaged, but I’ve got one last instance that I just can’t seem to get rid of. If I do the expected replacement, I wind up with “unable to resolve comptime value”.

What am I missing?

Thanks.

const std = @import("std");
const ba = @import("std");


const ff_array_list_test = false;

const UVK_SLAGGY_ACTION_FRAME_INDEX_COUNT_MAX = 4;
const MAX_ACTION_COUNT_PER_SLAGGY_INDEX = 8;

const SlaggyActions = ba.BoundedArray(u8, MAX_ACTION_COUNT_PER_SLAGGY_INDEX);


const SlaggyActionsSharedBA = ba.BoundedArray(SlaggyActions, UVK_SLAGGY_ACTION_FRAME_INDEX_COUNT_MAX);

const SlaggyActionsSharedAL = std.ArrayListUnmanaged(SlaggyActions);
var outer_buffer: [UVK_SLAGGY_ACTION_FRAME_INDEX_COUNT_MAX]SlaggyActions = undefined;


const SlaggyActionsShared = if (ff_array_list_test) SlaggyActionsSharedAL else SlaggyActionsSharedBA;



pub var slaggy_actions_shared: SlaggyActionsShared = if (!ff_array_list_test) blk: {
    // Bounded array implementation--compiles and works fine
    var sba: SlaggyActionsShared = SlaggyActionsShared.init(0) catch unreachable;

    for (0..UVK_SLAGGY_ACTION_FRAME_INDEX_COUNT_MAX) |_| {
        var sbai = SlaggyActions.init(0) catch unreachable;
        sbai.clear();  // FIXME: Why is this required to avoid local variable is never mutated?
        sba.appendAssumeCapacity(sbai);
    }

    break :blk sba;

} else blk: {
    // Array list implementation--this fails to compile with:
    // example.zig:44:67: error: unable to resolve comptime value
    //    var sba: SlaggyActionsShared = SlaggyActionsShared.initBuffer(outer_buffer);
    //                                                                  ^~~~~~~~~~~~
    // example.zig:23:54: note: initializer of container-level variable must be comptime-known
    // pub var slaggy_actions_shared: SlaggyActionsShared = if (!ff_array_list_test) blk: {
    //                                                      ^~

    var sba: SlaggyActionsShared = SlaggyActionsShared.initBuffer(outer_buffer);

    for (0..UVK_SLAGGY_ACTION_FRAME_INDEX_COUNT_MAX) |_| {
        var sbai = SlaggyActions.init(0) catch unreachable;
        sbai.clear();
        sba.appendAssumeCapacity(sbai);
    }

    break :blk sba;
};


pub fn main() void {
    if (ff_array_list_test) {
        slaggy_actions_shared.items[0].appendAssumeCapacity(1);
        const ff = slaggy_actions_shared.items[0].items[0];
        std.debug.print("{any}", .{ff}); 
    } else {
        slaggy_actions_shared.slice()[0].appendAssumeCapacity(1);
        const ff = slaggy_actions_shared.slice()[0].slice()[0];
        std.debug.print("{any}", .{ff});
    }
}

Godbolt link: Compiler Explorer

outer_buffer is value of array type and since it’s declared as var compiler can’t know if it was changed.
What you actually want is pointer to it which is known at compile time.

Replace:

var sba: SlaggyActionsShared = SlaggyActionsShared.initBuffer(outer_buffer);

with

var sba: SlaggyActionsShared = SlaggyActionsShared.initBuffer(&outer_buffer);

btw std.ArrayListUnmanaged was marked as deprecated in favor of std.ArrayList so it makes sense to update to std.ArrayList straight away.

        var sbai = SlaggyActions.init(0) catch unreachable;
        sbai.clear();  // FIXME: Why is this required to avoid local variable is never mutated?
        sba.appendAssumeCapacity(sbai);

Why you declaring sbai as var in first place shouldn’t it be const? It will prevent local variable is never mutated error.

1 Like

Erm, well that’s … annoying. :frowning:

I guess I’ll just go back to BoundedArray as a maintained module/external dependency then. This stuff is crossing thread boundaries and GPU frames and really can’t be piddling with allocators.

Sorry for the noise.

To be clear, std.ArrayListUnamanaged’s name was changed to std.ArrayList in 0.15, the previous ArrayList is now std.array_list.Managed.

4 Likes

Reminder that you can have this data structure if you want it… just because the standard library doesn’t endorse it enough to include it doesn’t mean you can’t have it.

5 Likes

Copy/paste is an underrated dependency management method.

6 Likes

Just adding a “&” to get &outer_buffer doesn’t seem to work. Now I get errors of:

/opt/compiler-explorer/zig-0.14.0/lib/std/array_list.zig:871:43: error: unable to evaluate comptime expression
            self.addOneAssumeCapacity().* = item;
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~
/opt/compiler-explorer/zig-0.14.0/lib/std/array_list.zig:871:40: note: operation is runtime due to this operand
            self.addOneAssumeCapacity().* = item;
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~^~
example.zig:49:33: note: called at comptime from here
        sba.appendAssumeCapacity(sbai);

Hmm not sure what is going own there.

You’re trying to manipulate a global non-comptime var (outer_buffer) at comptime.

When do you think the code inside blk: is supposed to run?

Well, that’s what the API of ArrayList is requiring of me, no?

The BoundedArray version does what I expect. The ArrayList one does not.

The question for which I am seeking the answer is:

Supposedly, ArrayList is the “officially blessed” replacement for BoundedArray, so how do I use ArrayList to do this replacement?

The seeming difficulty and confusion around this task suggests that there is a either 1) a significant functionality hole in the API of ArrayList or 2) a lurking bug of unspecified comptime behavior in the implementation of BoundedArray.

And both of those are above my pay grade. Which is why I asked here.

I see what you mean.

I think the way to go about it would be constructing the array at comptime, and then initializing the arraylist with fromOwnedSlice. Something like this:

const MAX = 32;

var array_comptime_constructed: [MAX]u32 = blk: {
    var buffer: [MAX]u32 = undefined;
    for (&buffer) |*b| {
        b.* = ...;
    }
    break :blk buffer;
};

var arraylist_comptime_constructed: std.ArrayList(u32) = .fromOwnedSlice(&array_comptime);

I’d also ask, since you seem to be filling the array completely at comptime, without any unused capacity, why you need an ArrayList and why a slice (or plain array) doesn’t suffice?

The problem is cut down–I don’t normally fill it completely. (Actually, maybe I do. :thinking: That wasn’t my intent, though …)

It’s a command queue being used in Vulkan. The outer array is the number of frames the swapchain has in flight while the inner array are the actual commands. The outer array normally only gets rebuilt on swapchain rebuilds where the number of frames in flight may readjust. The inner arrays are being used in various threads and the locking is being done by Vulkan timeline semaphores. They clear every time the frame gets recycled.

Sure, I could just keep track of len and use a straight array–but that’s exactly what BoundedArray does!

At this point I’m going to apply what I infer is part of the Zen of Zig: “Code it and move on.”

Thanks for all the help. Maybe someone more clever than me will find this an interesting puzzle and come up with something elegant.

1 Like

That’s fair. Sorry I couldn’t provide more useful or applicable help. From what I’ve seen the majority of issues that people have with replacing BoundedArray with ArrayList is the difference in how it interacts with comptime. I’m not affiliated with the zig project, just another user of the language, but for what it’s worth I think it’s an API worth revisiting at some point purely for that aspect (unless comptime semantics are revised again, but I doubt it due to the focus on incremental compilation).

But (not that my opinion really matters here) I also think revisiting that particular API should be far from a priority. Like @andrewrk said, you can just take the code and drop it in your project; it doesn’t have to be in the stdlib.

All that said, if you have comptime known bounds, then I do think the right abstraction is probably just a slice or array.

If there’s enough interest, it would be relatively straightforward to maintain a boneyard repo, one which gets updated for each major point release, preserving the API and data structures removed from previous ones.

That would enable a two-step upgrade process: change all references to use the boneyard, then update to the new hotness at leisure. Which could be as late as never, depending.

Not that ziglang should do this, rather the opposite: but as a community project it could be worthwhile.

It’s a nontrivial amount of work, but, if it had its own channel, monitoring the tip of master and moving things as they leave the rotation should spread that out.

2 Likes

There’s ziglang/std-lib-orphanage, though doesn’t seem like stuff ends up there anymore + that was specifically unmaintained (until someone decided to pick things up for a new package)

Seems like if there’s enough interest to maintain a deleted data structure, someone will make a package (like Frank did for BoundedArray)

1 Like

FWIW I simply ‘vendored’ a stripped down version of BoundedArray directly into my project with just the features I’m using, it ended up at 62 lines of code:

5 Likes

That seems very reasonable, and I’ll note that the typical argument of “but we want the standard library to have data structures that might be passed around in APIs” doesn’t apply here since it’s the standard library’s stance that BoundedArray is not a suitable data structure to be used in a reusable API. Or at least, it’s self-consistent.

2 Likes

It’s an interesting question whether any stdlib container is suitable to be passed across API boundaries, or whether slices are always enough… with containers you’re always also passing ‘behaviour’ around, which at least from my experience from C++ almost always turned out to be a bad idea down the line (for instance making refactoring harder).

1 Like

That’s a great point. On the other hand, it’s a quite versatile pattern to accept a mutable ArrayList and append to it. Then again, if that was an ArrayList(u8), then a *std.Io.Writer would be even better…

2 Likes