Happy Halloween!
I thought it would be fun to have challenge to see what some of the most Cursed zig you’ve caught yourself writing. Why did you write it? Do you regret it, or was it the only way?
Happy Halloween!
I thought it would be fun to have challenge to see what some of the most Cursed zig you’ve caught yourself writing. Why did you write it? Do you regret it, or was it the only way?
I’ve got one for you that I literally wrote earlier today:
/// Attempt to start a concurrent task, eventually erroring out if we can't start the task
/// after a stupidly high number of retries.
fn start_task(
io: std.Io, group: *std.Io.Group,
task: anytype, args: std.meta.ArgsTuple(@TypeOf(task))
) error{ConcurrencyUnavailable}!void {
const MAX_RETRIES = @as(usize, 1) << 12;
var retry_count: usize = 0;
// Convoluted way to add a task.
// We optimistically assume concurrency will only be unavailable if we have too
// many tasks going at once, so if it's unavailable we await the group, then try
// again.
// This is probably maximally cursed Zig code.
while(if(group.concurrent(io, task, args)) |_| false else |_| true) : (retry_count += 1) {
if(retry_count >= MAX_RETRIES) return error.ConcurrencyUnavailable;
group.await(io) catch std.log.err("Tasks were somehow cancelled!", .{});
}{}
}
When I was new to Zig I would spend a little bit too much time trying to abuse compile time duck typing to do stuff that Zig explicitly does not support. I think my crowning (dethroning?) achievement when I realized just how stupid it was getting was when I added ad-hoc polymorphism, the code itself I wouldn’t say is particularly cursed, but the idea is. I wrote about it here Ad Hoc Polymorphism in Zig – Ali Shahwali for anyone interested, below is the function that enables it for the lazy people ![]()
pub fn adHocPoly(comptime T: type, args: anytype, fns: anytype) T {
const args_ty = @typeInfo(@TypeOf(args));
const found_idx = blk: switch (args_ty) {
inline .@"struct" => |s| {
const fields = s.fields;
const fns_ty = @typeInfo(@TypeOf(fns));
switch (fns_ty) {
inline .@"struct" => |fns_struc| {
comptime for (fns_struc.fields, 0..) |func, fn_idx| {
const fn_ty = @typeInfo(func.type);
switch (fn_ty) {
inline .@"fn" => |fn_ty_info| {
if (fn_ty_info.params.len != fields.len) {
continue;
}
if (fn_ty_info.return_type) |RT| {
if (RT != T) {
continue;
}
} else {
@compileError("generic return type found, can not compare it to T");
}
var found = true;
for (fn_ty_info.params, 0..) |param, i| {
if (param.type) |param_ty| {
if (param_ty != fields[i].type) {
found = false;
break;
}
} else {
@compileError("generic fn parameter of anytype found, can not deduce which fn to call");
}
}
if (found) {
break :blk fn_idx;
}
},
else => @compileError("fns must be a tuple of functions, found " ++ @typeName(func.type)),
}
};
@compileError("no function which fulfills type signature of args was provided");
},
else => @compileError("fns must be a tuple of functions, was " ++ @typeName(@TypeOf(fns))),
}
},
else => @compileError("expected a tuple for args, was " ++ @typeName(@TypeOf(args))),
};
return @call(.auto, fns[found_idx], args);
}
I’ve never thought about how I would wait for a concurrency unit to be available…
The correction history heuristics of my chess program. I don’t even dare to link it.
Not that cursed, but I had a use for ?noreturn recently.
Hah, I’ve actually written a !noreturn recently.
Though I’d say a ?noreturn feels more cursed to me.
Can you share some more context? at first sight it seems to me that ?noreturn carries 0 bits of information and is ultimately the same as void.
It can be null!
I had a test-only hooks: ?Hooks thing, which was null in production, used as
if (self.hooks) |hooks| {
hooks.run();
}
And not only Hooks were test-only, they relied on some Unix hackery, and were a pain in the back to support on windows. So I changed it to
hooks: ?(if (native_os == .windows) noreturn else Hooks)
That way,
if (self.hooks) |hooks| {
hooks.run();
}
compiles both on unix and windows, as, in the latter case, compiler statically knows that the if is not taken.
I’ve been noodling on a hobby OS for small RISC-V SoC devices, and came up with a cursed way to test the exception handler of the interrupt service routine. The test wires up an “instruction skipper” handler that will record the trap code, then calculate the next instruction and set the exception program counter to it. This causes the machine to skip over a “bad” instruction, so a test can trigger an exception then check for the corresponding code:
test "isr exception" {
// restore the previous ISR handler after
const prev_handler = isr.exception_handlers[hartId()];
defer isr.exception_handlers[hartId()] = prev_handler;
// wire up the instruction skipper as the exception handler
var instruction_skipper: InstructionSkipper = .{};
isr.exception_handlers[hartId()] = &instruction_skipper.exceptionHandler();
const null_ptr: *allowzero volatile u8 = @ptrFromInt(0);
if (hal.csr.Satp.read().mode == .bare) {
// Load access fault
_ = null_ptr.*;
try std.testing.expect(instruction_skipper.scause.load(.acquire) == .load_access_fault);
// Store/AMO access fault
null_ptr.* = 0;
try std.testing.expect(instruction_skipper.scause.load(.acquire) == .store_access_fault);
} else {
// Load page fault
_ = null_ptr.*;
try std.testing.expect(instruction_skipper.scause.load(.acquire) == .load_page_fault);
// Store/AMO page fault
null_ptr.* = 0;
try std.testing.expect(instruction_skipper.scause.load(.acquire) == .store_page_fault);
}
// Illegal instruction
asm volatile ("unimp");
try std.testing.expect(instruction_skipper.scause.load(.acquire) == .illegal_instruction);
// Breakpoint
asm volatile ("ebreak");
try std.testing.expect(instruction_skipper.scause.load(.acquire) == .breakpoint);
// Environment call from S-mode
asm volatile ("ecall");
try std.testing.expect(instruction_skipper.scause.load(.acquire) == .smode_environment_call);
}
pub const InstructionSkipper = struct {
// handler is "called" via the interrupt, but the compiler doesn't see
// that, so it may cache the value in a register in release mode unless
// scause is an atomic load which adds a memory barrier.
scause: std.atomic.Value(hal.csr.Scause.RegisterT) = .init(undefined),
pub fn exceptionHandler(self: *const InstructionSkipper) ExceptionHandler {
return .{
.ctx = @ptrCast(@constCast(self)),
.isr = InstructionSkipper.handler,
};
}
fn handler(ctx: *anyopaque, exception: *const Isr.Exception) void {
var self: *@This() = @ptrCast(@alignCast(ctx));
self.scause.store(exception.scause, .release);
const instruction: *const u16 = @ptrFromInt(exception.sepc);
// if lowest 2 bits != 0b11, it's a 2-byte compressed instruction
const len: usize = if ((instruction.* & 0b11) != 0b11) 2 else 4;
hal.csr.Sepc.write(exception.sepc + len);
}
};
I think the eleven levels of indentation in this function also shouldn’t be ignored here. Spaces make up over half of that entire code block (51.5% to be exact).
const U = union(enum) { err: WsError!noreturn };
but have you written a union with only one option that is an error union of noreturn? It’s because Io.Select requires a union. Better yet, one of WsError’s values is Clean, meaning that there’s no error handling to be done. It’s subtly different than just returning void in that it means there was an error, the multiple concurrent processes need to be all shut down/cancelled, however I’ve already done all of the error reporting/logging, so just clean up your call stack.
Gotta prefault those stack pages somehow:
pub noinline fn probeStack(comptime size: usize) void {
var big: [size]u8 = undefined;
const big_ptr: *volatile [size]u8 = &big;
big_ptr.* = @splat(0xbb);
}
Curious, doesn’t Zig’s default stack prober do good enough job?
Just following the recommendations of this man page for touching the stack to lock pages for real time application:
I am mixed on if this is cursed.
I was working on something that required me to keep a bunch of indexes into a couple of lists and doing just
const IndexType = u32
const IndexType2 = u32
Was not good enough because I kept mixing them up since they ware all being reduced to u32 and the compiler just went went it.
So I did this
const IndexType = enum(u32) { _ };
const IndexType2 = enum(u32) { _ };
That way when I put it as a function parameter the compiler stopped me if I put in the wrong one.
Not sure if this is in the spirit of the question, but I wrote a Fizz buzz using async functions and std.Io.Queues: Queue
It’s not that complicated, just much more complicated than Fizz buzz needs to be ![]()
Not sure if this is truly cursed as it’s a legit workaround for a genuine compiler/LLVM flaw rather than some dumb-smart nonsense, but any code that should be simple but instead requires half a dozen @as and a long comment that explains why its existence is justified for is probably some amount of cursed.
Many instruction sets have SIMD packing instruction for converting two registers’ worth of 16/32/64-bit integers into one register’s worth of the next smallest integer type using saturating conversion, i.e. converting [16]u16 to [16]u8 by clamping large values like 1000 to 255.
When targeting x64 and the default/baseline CPU, the instruction we want is packuswb (pack with unsigned saturation, word to byte):
saturate:
push rbp
mov rbp, rsp
packuswb xmm0, xmm1
pop rbp
ret
Zig doesn’t (currently) have anything like a @saturate builtin as a counterpart to @truncate, but for a scalar value you can achieve the equivalent with @min(x, 255), so you would expect the following to work for vectors:
fn saturate(vec: @Vector(16, u16)) @Vector(16, u8) {
return @min(vec, @as(@Vector(16, u16), @splat(std.math.maxInt(u8))))
}
Unfortunately, the above will produce the following output, which (while correct) is much less efficient and not at all what we want:
.LCPI1_0:
.short 255
.short 255
.short 255
.short 255
.short 255
.short 255
.short 255
.short 255
saturate:
push rbp
mov rbp, rsp
movdqa xmm2, xmmword ptr [rip + .LCPI1_0]
movdqa xmm3, xmm1
psubusw xmm3, xmm2
psubw xmm1, xmm3
movdqa xmm3, xmm0
psubusw xmm3, xmm2
psubw xmm0, xmm3
packuswb xmm0, xmm1
pop rbp
ret
The fix? To get the desired output with a single packuswb instruction, we need the following monstrosity:
fn saturate(vec: @Vector(16, u16)) @Vector(16, u8) {
// The only reliable way to get Zig/LLVM to emit efficient
// unsigned saturating conversion instructions is
// via the LLVM IR pattern 'truncate(smin(smax(x, 0), 255))'
// (the order of 'smin' and 'smax' and arguments can be swapped).
// Only doing 'truncate(umin(x, 255))' is not enough,
// which means that the integer type must remain signed
// at all points in the expression below in order for
// Zig to not elide any seemingly unnecessary operations.
return @bitCast(
@as(@Vector(16, i8), @truncate(
@as(@Vector(16, i16), @max(
@as(@Vector(16, i16), @splat(std.math.minInt(u8))),
@as(@Vector(16, i16), @min(
@as(@Vector(16, i16), @bitCast(vec)),
@as(@Vector(16, i16), @splat(std.math.maxInt(u8))),
)),
)),
)),
);
}
LLVM and Zig are kinda working against each other here; LLVM’s pattern matching is a bit too rudimentary by only caring for signed types, but Zig’s @min/@max is being a bit too eager in this situation by constantly coercing the result to the most narrow type possible.
Discovering this fix was frustrating, it took me over a day of scouring random GitHub issues and Zig compiler/LLVM source code to finally discover the magic incantation required for the output I wanted without needing to fall back to asm volatile.
Happy Halloween! ![]()
Not cursed Zig but as a new user I wanted to express my excitement via pumpkin carving and this felt like the right place
I think that you can be freed of that curse. Devlog ⚡ Zig Programming Language