Skip to content

Conversation

andrewrk
Copy link
Member

@andrewrk andrewrk commented Sep 10, 2025

The comptime allocator allocates into .data / .cdata and relies on linker garbage collection by using global (array) variables. This is better than using std.heap.FixedBufferAllocator because it does not cause unused data within the fixed buffer to be part of the program's address space.

First commit: allow coercion of generic function pointer to runtime function pointer when comptime-known.

Intended to close #1291 but does not accomplish it yet:

/home/andy/src/zig/lib/std/mem/Allocator.zig:142:20: error: unable to resolve comptime value
    return a.vtable.alloc(a.ptr, len, alignment, ret_addr);
           ~~~~~~~~^~~~~~
/home/andy/src/zig/lib/std/mem/Allocator.zig:296:35: note: called at comptime from here
    const byte_ptr = self.rawAlloc(byte_count, alignment, return_address) orelse return Error.OutOfMemory;
                     ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/lib/std/mem/Allocator.zig:282:40: note: called at comptime from here
    return self.allocBytesWithAlignment(alignment, byte_count, return_address);
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/lib/std/mem/Allocator.zig:270:89: note: called at comptime from here
    const ptr: [*]align(a.toByteUnits()) T = @ptrCast(try self.allocWithSizeAndAlignment(@sizeOf(T), a, n, return_address));
                                                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/lib/std/mem/Allocator.zig:258:41: note: called at comptime from here
    return self.allocAdvancedWithRetAddr(T, alignment, n, @returnAddress());
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/lib/std/multi_array_list.zig:491:51: note: called at comptime from here
            const new_bytes = try gpa.alignedAlloc(u8, .of(Elem), capacityInBytes(new_capacity));
                                  ~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/lib/std/multi_array_list.zig:460:36: note: called at comptime from here
            return self.setCapacity(gpa, growCapacity(self.capacity, new_capacity));
                   ~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/lib/std/array_hash_map.zig:892:49: note: called at comptime from here
            try self.entries.ensureTotalCapacity(gpa, new_capacity);
                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/lib/std/array_hash_map.zig:787:44: note: called at comptime from here
            self.ensureTotalCapacityContext(gpa, self.entries.len + 1, ctx) catch |err| {
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/lib/std/array_hash_map.zig:775:56: note: called at comptime from here
            const gop = try self.getOrPutContextAdapted(gpa, key, ctx, ctx);
                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/lib/std/array_hash_map.zig:942:52: note: called at comptime from here
            const result = try self.getOrPutContext(gpa, key, ctx);
                               ~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~
/home/andy/src/zig/lib/std/array_hash_map.zig:261:45: note: called at comptime from here
            return self.unmanaged.putContext(self.allocator, key, value, self.ctx);
                   ~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
test.zig:9:12: note: called at comptime from here
    map.put(12, 34) catch unreachable;
    ~~~~~~~^~~~~~~~
test.zig:7:1: note: 'comptime' keyword forces comptime evaluation
comptime {
^~~~~~~~
referenced by:
    root: /home/andy/src/zig/lib/std/start.zig:3:22
    comptime: /home/andy/src/zig/lib/std/start.zig:31:9
    2 reference(s) hidden; use '-freference-trace=4' to see all references

Next step is to figure out why this compile error incorrectly thinks the alloc function is not comptime known.

Merge Checklist

  • Also introduce error for when pointer to inline/generic function becomes runtime-known
  • Add comptime allocator implementation to std lib
  • Add test coverage for comptime allocator

to runtime function pointer when comptime-known.
@andrewrk andrewrk added the release notes This PR should be mentioned in the release notes. label Sep 10, 2025
@mlugg
Copy link
Member

mlugg commented Sep 10, 2025

The implementation you propose in #1291 is not valid:

  • The inline annotation has no effect: under Zig's modern type equivalence rules (which were introduced to avoid pointless over-instantiation and to make incremental compilation more tractable) the struct types arising from different calls are still distinct types.
  • Even if that did work, you... can't mutate a global var at comptime? Those are runtime variables. There's no such thing as a global comptime var; that's something we've been very firm on.

This PR is also not valid. In order to do what you're trying to do, you would need to check that this coerced function never becomes runtime-known. This is a task we have so far designed the language rules to avoid needing, because it's a lot of work: the problem is aggregate values, which can become runtime-known and then require implementing a recursive check through all sub-values. This is because this language change is just a hack: inline functions are not normal functions.

Luckily for you, it's not even useful for #1291. We know this because comptime function calls do not care whether or not a function is marked as inline: comptime calls and inline calls are essentially the same thing. Comptime calls are subject to memoization, but in modern Zig that is purely an implementation detail: no language semantics change without function call memoization.

Comptime allocators work in Zig today---no language changes needed(*). Here is an implementation with usage: https://zigbin.io/2e8af7

The tradeoff is that, for correctness reasons, you cannot access the memory at runtime (everyone's favourite "runtime value contains reference to comptime var" error). This tradeoff is one we agreed on around a year ago when I rewrote the comptime memory access logic; the alternatives are much more complex, so we chose this approach. If the use case of using comptime_allocator memory at runtime is considered sufficiently compelling, we can explore another design direction there, but it will introduce language complexity.

Either way, this diff is both incorrect (a lot more complex logic is required to make this change sound) and pointless (it makes no difference to #1291); I strongly believe this should be closed.


(*): there are currently some caveats related to how Zig handles comptime memory. The biggest one is that Zig does not currently allow reinterpreting a [@sizeOf(T)]u8 to an arbitrary T in memory at comptime, which effectively makes it impossible to use comptime_allocator with ill-defined layout types (e.g. auto-layout structs). It is intended that we will lift this restriction in the future. But this is not an issue with comptime_allocator itself; rather, with comptime memory in general.

@tristanpemble
Copy link

tristanpemble commented Sep 10, 2025

this might not be the right place for me to share this, so let me know if I should put it elsewhere.

I think @andrewrk started work on this PR after a conversation with me on IRC. I am here to share the use case I had and the experience I had trying different approaches, in case it helps inform the discussion. if not, feel free to disregard.

I've been messing around with task scheduling graphs in comptime for fun. ultimately I need some kind of growable comptime list, and nodes would need to store function pointers.

I just tried to use your allocator, @mlugg, and I quickly ran into the caveat you outlined at the end of your comment, which I now fully comprehend after experiencing it first hand. the Node type required extern struct for a defined layout, which meant I cannot store function pointers. I tried to use callconv(.c) function pointers, but that resulted in a compiler crash. if you think this is an unreported bug, I can try to recreate it and file appropriately.

ultimately I just implemented my own dumb ComptimeList type, instead of trying to use std.ArrayList. the working result of my experiment is here: https://zigbin.io/374358/run

@mlugg
Copy link
Member

mlugg commented Sep 11, 2025

I quickly ran into the caveat you outlined at the end of your comment

Yeah, it's an unfortunate problem which really limits where you can use Allocator at comptime right now. But there's literally no comptime Allocator implementation which could bypass this---the compiler's restrictions on comptime memory are currently fundamentally at odds with the std.mem.Allocator interface.

Luckily, this is absolutely solvable. It just requires some language and compiler design work, which I unfortunately won't be able to get to for a while; sorry about that.

which meant I cannot store function pointers

If you need it, a possible workaround would be storing them as *const anyopaque and casting back to the right type to call them. To be clear, I'm not saying I view status quo as acceptable---just giving a workaround in case it helps you!

I tried to use callconv(.c) function pointers, but that resulted in a compiler crash.

Hmm, I'm certainly not aware of any such bug off the top of my head. So yeah, if you would be able to repro it and file an issue, that would be appreciated.

ultimately I just implemented my own dumb ComptimeList type, instead of trying to use std.ArrayList

Yeah, that's a good idea for the time being. To be honest, the main use case for a comptime allocator is to reuse existing code which uses allocators, e.g. parsing file formats at comptime by just calling into a normal runtime parsing library. If you're writing comptime-only code in the first place, then you can easily do what you did here and just use comptime-specific abstractions. FWIW, when I'm doing hacky comptime stuff (that never sees the light of day!), here's my go-to implementation---it's basically just a slightly more featureful version of yours:

comptime_array_list.zig
fn ComptimeArrayList(comptime T: type) type {
    return struct {
        items: []T,
        capacity: usize,

        const Self = @This();

        pub const empty: Self = .{ .items = &.{}, .capacity = 0 };

        fn append(cal: *Self, val: T) void {
            cal.ensureCapacity(cal.items.len + 1);
            cal.items.ptr[cal.items.len] = val;
            cal.items.len += 1;
        }

        fn appendSlice(cal: *Self, vals: []const T) void {
            cal.ensureCapacity(cal.items.len + vals.len);
            @memcpy(cal.items.ptr[cal.items.len..], vals);
            cal.items.len += vals.len;
        }

        fn ensureCapacity(cal: *Self, need_capacity: usize) void {
            if (cal.capacity >= need_capacity) return;
            var cap: usize = @max(cal.capacity, 16);
            while (cap < need_capacity) cap *= 2;
            var new_items: [cap]T = undefined;
            @memcpy(new_items[0..cal.items.len], cal.items);
            cal.items = new_items[0..cal.items.len];
            cal.capacity = cap;
        }
    };
}

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.

use case: comptime allocator
3 participants