At runtime, we can create two mutually referencing pointers like this:
test "runtime" {
var x: *const anyopaque = undefined;
var y: *const anyopaque = undefined;
x = @ptrCast(&y);
y = @ptrCast(&x);
try std.testing.expectEqual(@intFromPtr(x), @intFromPtr(&y));
try std.testing.expectEqual(@intFromPtr(y), @intFromPtr(&x));
}
However, when I attempt to create them at compile time, the following error occurs:
test "comptime" {
const x, const y = comptime blk: {
var x: *const anyopaque = undefined;
var y: *const anyopaque = undefined;
x = @ptrCast(&y);
y = @ptrCast(&x);
break :blk .{ x, y };
};
try std.testing.expectEqual(@intFromPtr(x), @intFromPtr(&y));
try std.testing.expectEqual(@intFromPtr(y), @intFromPtr(&x));
}
error: runtime value contains reference to comptime var
try std.testing.expectEqual(@intFromPtr(x), @intFromPtr(&y));
How can this be achieved?
TIPS: The code above is minimized for testing purposes; my goal is conceptually equivalent to creating a doubly linked list at compile time.
AFAIK It does not make sense to take the adress of a variable at compile time: it does not live in memory. Maybe using an array of structs, and indices would solve your issues ? The cool thing is that you can even add objects to the array at compile time using ++, no need to worry about allocation failures.
That’s a separate matter; this thread is intended to discuss pointer referencing.
When I force a change to the constant value, new problems arise.
test "comptime2" {
const x, const y = comptime blk: {
const x: *const anyopaque = undefined;
const y: *const anyopaque = undefined;
const addr_x: *@TypeOf(x) = @constCast(&x);
const addr_y: *@TypeOf(y) = @constCast(&y);
addr_x.* = @ptrCast(addr_y);
addr_y.* = @ptrCast(addr_x);
break :blk .{ x, y };
};
try std.testing.expectEqual(@intFromPtr(x), @intFromPtr(&y));
try std.testing.expectEqual(@intFromPtr(y), @intFromPtr(&x));
}
error: unable to evaluate comptime expression
addr_x.* = @ptrCast(addr_y);
~~~~~~~~~^~~~~~~~~~~~~~~~~~
rpkak
March 13, 2026, 12:48pm
4
const std = @import("std");
comptime {
var x: *const anyopaque = undefined;
const y: *const anyopaque = @ptrCast(&x);
x = @ptrCast(&y);
@compileLog(x); // @as(*const anyopaque, @as(*const anyopaque, @ptrCast(&@as(*const anyopaque, @as(*const anyopaque, @ptrCast(&@as(*const anyopaque, @as(*const anyopaque, @ptrCast(&@as(*const anyopaque, @as(*const anyopaque, @ptrCast(&@as(*const anyopaque, ...)))))))))))))
@compileLog(y); // @as(*const anyopaque, @as(*const anyopaque, @ptrCast(&@as(*const anyopaque, @as(*const anyopaque, @ptrCast(&@as(*const anyopaque, @as(*const anyopaque, @ptrCast(&@as(*const anyopaque, @as(*const anyopaque, @ptrCast(&@as(*const anyopaque, ...)))))))))))))
@compileLog(x == y); // false
@compileLog(@as(*const anyopaque, @ptrCast(&x)) == y); // true
@compileLog(x == @as(*const anyopaque, @ptrCast(&y))); // true
@compileLog(@as(*const anyopaque, @ptrCast(&x)) == @as(*const anyopaque, @ptrCast(&y))); // false
}
Edit: the comments are the output with zig master. zig 0.15 segfaults with this code.
Comptime is strictly type-safe, so no casting allowed. Also, mutability in comptime cannot cross into runtime. You can create a mutable comptime value, but you need to do your processing and then crystallize it into a constant value, before it an be passed into runtime. From a logical perspective, you can imagine that all your comptime values will live in the constant data section of your binary. You can mutate it while the program is being compiled, but by the time it is executed, it needs to be frozen.
I don’t think you can achieve a doubly-linked list under these restrictions.
In order to create variables at comptime, you need to define a namespace:
test "comptime" {
const ns = struct {
var x: *const anyopaque = @ptrCast(&y);
var y: *const anyopaque = @ptrCast(&x);
};
try std.testing.expectEqual(@intFromPtr(ns.x), @intFromPtr(&ns.y));
try std.testing.expectEqual(@intFromPtr(ns.y), @intFromPtr(&ns.x));
}
To create a singly linked list:
test "linked list" {
const Node = struct {
next: ?*@This(),
pub fn init(comptime sibling: ?*@This()) *@This() {
const Self = @This();
const ns = struct {
var self: Self = .{ .next = sibling };
};
return &ns.self;
}
};
const ns = struct {
const root = Node.init(Node.init(Node.init(Node.init(null))));
};
std.debug.print("{}\n", .{ns.root});
}
I don’t think you can programmatically create a doubly linked list at comptime, since the definition of the namespaces happens in sequence and there isn’t a way to go back and change the initial value of a variable after it’s declared.