Halloween Challenge: Cursed Zig

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?

13 Likes

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!", .{});
	}{}
}
2 Likes

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 :smiley:

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);
}
3 Likes

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.

8 Likes

Hah, I’ve actually written a !noreturn recently.
Though I’d say a ?noreturn feels more cursed to me.

1 Like

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.

6 Likes

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).

1 Like
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);
}
2 Likes

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:

1 Like

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 :slight_smile:

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.

5 Likes

Happy Halloween! :jack_o_lantern:

Not cursed Zig but as a new user I wanted to express my excitement via pumpkin carving and this felt like the right place

18 Likes

I think that you can be freed of that curse. Devlog ⚡ Zig Programming Language

1 Like