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!
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!
// do-while loop
while (true) : (_ = condition or break) {
work();
}
The comptime infinite loop:
comptime var i = 0;
inline while (true) {
i += 1;
@setEvalBranchQuota(i);
}
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);
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!
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)
const std = @import("std");
pub fn build(b: *std.Build) void {
const buildStep = b.addSystemCommand(&.{ "zig", "build" });
b.getInstallStep().dependOn(&buildStep.step);
}
Canoncail infinite loop:
pub const main = loop: switch ({}) {
else => continue :loop {
@compileLog(
\\All the work and no play
\\makes Alex a dull boy
\\
);
},
};
What is truth?
while (true) break else 2 + 2 == 5;
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");
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);
}
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;
}
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.
@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));
}
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.
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.