Ministry of silly loops

It’s 3AM here, and, for whatever reason, I cannot sleep because I am thinking about Zig loops. Silly Zig loops specifically.

What is the silliest Zig loop you can manage? One loop per post please!

9 Likes
// do-while loop
while (true) : (_ = condition or break) {
    work();
}
18 Likes

The comptime infinite loop:

comptime var i = 0;
inline while (true) {
    i += 1;
    @setEvalBranchQuota(i);
}
16 Likes

If we think about silly ways to make loops, then tail-call recursion definitely belongs here:

struct {fn loop(i: u32, result: u32) u32 {
    if(i == 0) return result;
    return @call(.tail, loop, .{i - 1, result*i});
}}.loop(10, 1);
8 Likes

Stay a while and listen:

var i: u32 = 0;
while (i < 10) {
    defer i += 1;
    print("{d}\n", .{i})
}

Tricked you, this one is a serious loop, not a silly loop!

I think while (condition) : (increment) loops are redundant and should be removed from the language.

while (condition) : (increment) {
    body;
}

is, as far as I can tell at 3AM, fully equivalent to

while (condition) {
    defer increment;

    body;
}

But the latter is so much more readable! You don’t need to guess semantics of ) : ( and how it interacts with continue and what not. And you know why is that hard to intuit? Because it’s the single violation of “no control flow without keyword” principle! : is control flow, and a particularly tricky one! Execution goes from the condition, past the increment, to the loop body, and then back to the increment again.

while (condition) : (increment) loops are a bit more concise than while-defer loops, and that was important when a counting for loop was written as a while. But, since we have explicit counting for(0..10) loops, I’d say, down with the while with increment! Даёшь while defer!

8 Likes

I submit src/crash_report.zig made by spex_guy. Here’s some example stack frames from lldb:

frame #5: 0x000000000180f26d zig`crash_report.PanicSwitch.abort at crash_report.zig:491:26
   488      }
   489 
   490      noinline fn abort() noreturn {
-> 491          std.process.abort();
   492      }
   493 
   494      inline fn goTo(comptime func: anytype, args: anytype) noreturn {
(lldb) 
frame #6: 0x000000000185d7b9 zig`goTo((null)=<unavailable>) at crash_report.zig:499:9
   496          // to avoid blowing up the stack.  It's ok for now though, there are no
   497          // cycles in the state machine so the max stack usage is bounded.
   498          //@call(.always_tail, func, args);
-> 499          @call(.auto, func, args);
   500      }
   501 
   502      fn recover(
(lldb) 
frame #7: 0x000000000185d7b4 zig`crash_report.PanicSwitch.releaseRefCount(state=0x0000000006203028) at crash_report.zig:472:13
   469              @panic("event.wait() returned");
   470          }
   471 
-> 472          goTo(abort, .{});
   473      }
   474 
   475      noinline fn recoverAbort(
(lldb) 
frame #8: 0x0000000001814390 zig`goTo(args=struct { *volatile crash_report.PanicSwitch.PanicState } @ 0x00007ffffffbd058) at crash_report.zig:499:9
   496          // to avoid blowing up the stack.  It's ok for now though, there are no
   497          // cycles in the state machine so the max stack usage is bounded.
   498          //@call(.always_tail, func, args);
-> 499          @call(.auto, func, args);
   500      }
   501 
   502      fn recover(
(lldb) 
frame #9: 0x0000000001814372 zig`crash_report.PanicSwitch.releaseMutex(state=0x0000000006203028) at crash_report.zig:444:13
   441 
   442          std.debug.unlockStdErr();
   443 
-> 444          goTo(releaseRefCount, .{state});
   445      }
   446 
   447      noinline fn recoverReleaseRefCount(
(lldb) 
frame #10: 0x000000000185ce82 zig`goTo(args=struct { *volatile crash_report.PanicSwitch.PanicState } @ 0x00007ffffffbd150) at crash_report.zig:499:9
   496          // to avoid blowing up the stack.  It's ok for now though, there are no
   497          // cycles in the state machine so the max stack usage is bounded.
   498          //@call(.always_tail, func, args);
-> 499          @call(.auto, func, args);
   500      }
   501 
   502      fn recover(
(lldb) 
frame #11: 0x000000000185ce55 zig`crash_report.PanicSwitch.reportStack(state=0x0000000006203028) at crash_report.zig:426:13
   423          }
   424          state.panic_ctx.dumpStackTrace();
   425 
-> 426          goTo(releaseMutex, .{state});
   427      }
   428 
   429      noinline fn recoverReleaseMutex(
(lldb) 
frame #12: 0x000000000180e958 zig`goTo(args=struct { *volatile crash_report.PanicSwitch.PanicState } @ 0x00007ffffffbd410) at crash_report.zig:499:9
   496          // to avoid blowing up the stack.  It's ok for now though, there are no
   497          // cycles in the state machine so the max stack usage is bounded.
   498          //@call(.always_tail, func, args);
-> 499          @call(.auto, func, args);
   500      }
   501 
   502      fn recover(
(lldb) 
frame #13: 0x000000000180e92b zig`crash_report.PanicSwitch.initPanic(state=0x0000000006203028, trace=?*const builtin.StackTrace @ 0x00007ffffffbd1a8, stack=StackContext @ 0x00007ffffffbd1b0, msg=len=5) at crash_report.zig:400:13
   397              stderr.print("\nIntercepted error.{} while dumping current state.  Continuing...\n", .{err}) catch {};
   398          };
   399 
-> 400          goTo(reportStack, .{state});
   401      }
   402 
   403      noinline fn recoverReportStack(
(lldb) 
frame #14: 0x0000000001807dee zig`goTo(args=struct { *volatile crash_report.PanicSwitch.PanicState, ?*const builtin.StackTrace, crash_report.StackContext, []const u8 } @ 0x00007ffffffbd550) at crash_report.zig:499:9
   496          // to avoid blowing up the stack.  It's ok for now though, there are no
   497          // cycles in the state machine so the max stack usage is bounded.
   498          //@call(.always_tail, func, args);
-> 499          @call(.auto, func, args);
   500      }
   501 
   502      fn recover(
(lldb) 
frame #15: 0x0000000001807cfc zig`crash_report.PanicSwitch.dispatch(trace=?*const builtin.StackTrace @ 0x00007ffffffbd480, stack_ctx=StackContext @ 0x00007ffffffbd488, msg=len=5) at crash_report.zig:353:32
   350          debug.assert(panic_state.awaiting_dispatch);
   351          panic_state.awaiting_dispatch = false;
   352          nosuspend switch (panic_state.recover_stage) {
-> 353              .initialize => goTo(initPanic, .{ panic_state, trace, stack_ctx, msg }),
   354              .report_stack => goTo(recoverReportStack, .{ panic_state, trace, stack_ctx, msg }),
   355              .release_mutex => goTo(recoverReleaseMutex, .{ panic_state, trace, stack_ctx, msg }),
   356              .release_ref_count => goTo(recoverReleaseRefCount, .{ panic_state, trace, stack_ctx, msg }),
(lldb) 
frame #16: 0x00000000017fd0b4 zig`crash_report.compilerPanic(msg=len=5, maybe_ret_addr=?usize @ 0x00007ffffffbdad0) at crash_report.zig:153:25
   150      PanicSwitch.preDispatch();
   151      const ret_addr = maybe_ret_addr orelse @returnAddress();
   152      const stack_ctx: StackContext = .{ .current = .{ .ret_addr = ret_addr } };
-> 153      PanicSwitch.dispatch(@errorReturnTrace(), stack_ctx, msg);
   154  }
   155 
   156  /// Attaches a global SIGSEGV handler
(lldb) 
2 Likes
const std = @import("std");

pub fn build(b: *std.Build) void {
    const buildStep = b.addSystemCommand(&.{ "zig", "build" });
    b.getInstallStep().dependOn(&buildStep.step);
}
6 Likes

Canoncail infinite loop:

pub const main = loop: switch ({}) {
    else => continue :loop {
        @compileLog(
            \\All the work and no play
            \\makes Alex a dull boy
            \\
        );
    },
};
1 Like

What is truth?

while (true) break else 2 + 2 == 5;
1 Like

Iterating (needlessly) over multi-item pointer of zero-sized constant values:

pub fn main() !void {
    var sum: u32 = 0;
    for (0..10, infinity(5)) |_, i| {
        sum += i.value;
    }
    std.debug.print("sum: {}\n", .{sum});
}

pub fn infinity(comptime value: anytype) [*]const ConstValue(value) {
    return @ptrCast(&bytes);
}
const bytes: [1]u8 = undefined;

fn ConstValue(comptime value: anytype) type {
    return struct {
        comptime value: @TypeOf(value) = value,
    };
}

const std = @import("std");
2 Likes

Another do-while loop:

test "do-while" {
    var i: u8 = 'a';
    var result: std.ArrayListUnmanaged(u8) = .empty;
    defer result.deinit(std.testing.allocator);
    while (do_blk: {
        i += 1;
        try result.append(std.testing.allocator, i);
        break :do_blk i != 'd';
    }) {}
    try std.testing.expectEqualStrings("bcd", result.items);
}
1 Like
const std = @import("std");

var i: usize = undefined;

pub fn panic(_: anytype, _: anytype, _: anytype) noreturn {
    var buf: [std.fs.max_path_bytes]u8 = undefined;
    const path = std.fs.selfExePath(&buf) catch unreachable;
    const i_str = std.fmt.allocPrint(std.heap.page_allocator, "{}", .{i + 1}) catch unreachable;
    std.process.execv(std.heap.page_allocator, &.{ path, i_str }) catch unreachable;
}

pub fn main() noreturn {
    const args = std.process.argsAlloc(std.heap.page_allocator) catch unreachable;

    i = if (args.len == 1) 0 else std.fmt.parseInt(usize, args[1], 10) catch unreachable;

    std.debug.print("{}\n", .{i});
    unreachable;
}
2 Likes

I found a very weird one with a labeled switch continue. It gets stuck on 0.14.1 but it’s fine on master, so it’s presumably patched.

const Pass = enum { one, two, };
fn huhPass(char: u8, comptime pass: Pass) void {
	sw: switch (char) {
		'1' => switch (pass) {
			.one => {},
			.two => continue :sw '2',
		},
		else => {},
	}
}
pub fn main() !void {
	huhPass('a', .one); // stuck
	huhPass('a', .two); // fine
}

Note that it only gets stuck when the continue is omitted.

2 Likes

@typeInfo() switches put evil ideas in my mind.

/// Repeatedly deference a pointer type and count how many levels down we traversed.
/// Technically returns a comptime-known constant.
pub fn pointer_dereference_loop(ptr: anytype) usize {
	comptime var counter: usize = 0;
	comptime loop: switch(@typeInfo(@TypeOf(ptr))){
		.pointer => |deref| {
			counter += 1;
			continue :loop @typeInfo(deref.child);
		},
		else => {
			break :loop;
		},
	};
	return counter;
}

test pointer_dereference_loop {
	const cursed_ptr: *const *const *const *const *const u8 = &&&&&32;
	try std.testing.expect(pointer_dereference_loop(cursed_ptr) == @as(usize, 5));
}

I know a lot of you will be disappointed that this loop is executed at comptime only, so here’s a runtime version using recursion:

pub fn pointer_dereference_loop_runtime(ptr: anytype, counter: ?usize) usize {
	const local_counter: usize = counter orelse 0;
	switch(@typeInfo(@TypeOf(ptr))){
		.pointer => return pointer_dereference_loop_runtime(ptr.*, local_counter + 1),
		else => return local_counter,
	}
}

test pointer_dereference_loop_runtime {
	const cursed_ptr: *const *const *const *const *const u8 = &&&&&32;
	try std.testing.expect(pointer_dereference_loop_runtime(cursed_ptr, null) == @as(usize, 5));
}
2 Likes

A translation into Zig of a silly loop I wrote in college.

while(true){
  while(true){
    if(trySomething()){
      continue;
    }
    // more if checks
    break;
  }

  if(trySomethingElse()){
    continue;
  }
  break;
}

Tricked you, I did not in fact translate this to Zig, I merely simplified the original Java. On a side note, thank goodness neither school nor work can compel me to write Java anymore.

This loop structure, while silly with the doubly nested while true with a break at the end, was actually quite useful. Basically, the code was attempting to find continuous paths in a grid. It would first try to find a neighbor locally, and if it did it continued the inner local loop. If it couldn’t find something adjacent, it would break into the outer loop where it would search anywhere for a open cell, at which point it would search locally around that cell.

1 Like

The Cleese Construct:

const std = @import("std");

pub fn main() void {
    const max: u2 = 2;

    var t: usize = 0;
    while (t < 100) : (for (0..max) |i| {
        t = t + i;
    }) t = t + 1;

    std.debug.print("It's {d}\n", .{t});
}

I find it rather silly.

2 Likes