Zig Notes

confirming code generation
Login

confirming code generation

Zig avoids lots of mysterious behavior that's common in other languages, but it's still possible to not know exactly what's going on in some code.

confirm types by making them explicit

If you think a variable has a particular type, which is currently inferred, you can fill it out. Zig will either make a safe coercion to your type, or disagree at compile-time:

pub fn main() !void {}

test "what type is f?" {
    const f = main;
    _ = f;
}

test "we know the type because this compiles" {
    const f: fn () anyerror!void = main;
    _ = f;
}

And if you put the wrong type, you get an slightly hard to read but still helpful error:

types.zig:9:35: error: expected type 'fn () anyerror!i32', found 'fn () @typeInfo(@typeInfo(@TypeOf(types.main)).Fn.return_type.?).ErrorUnion.error_set!void'
    const f: fn () anyerror!i32 = main;
                                  ^~~~
types.zig:9:35: note: return type '@typeInfo(@typeInfo(@TypeOf(types.main)).Fn.return_type.?).ErrorUnion.error_set!void' cannot cast into return type 'anyerror!i32'
types.zig:9:35: note: error union payload 'void' cannot cast into error union payload 'i32'

confirm types with @compileLog

If you don't know enough to put a good type and would like slightly more helpful output, or answers to more than one such question, you can directly ask Zig what it inferred for the type:

pub fn main() !void {}

test "what type is f?" {
    const f = main;
    @compileLog(f);
}

Which always halts compilation and provides some output:

types.zig:5:5: error: found compile log statement
    @compileLog(f);
    ^~~~~~~~~~~~~~

Compile Log Output:
@as(fn () @typeInfo(@typeInfo(@TypeOf(types.main)).Fn.return_type.?).ErrorUnion.error_set!void, (function 'main'))

compiling Zig to C

Consider that this returns without error:

const std = @import("std");

fn f(n: i32) i32 {
    var n1 = n;
    defer n1 += 1;
    return n1;
}
fn g(n: i32, m: i32) struct { i32, i32 } {
    var n1 = n;
    var m1 = m;
    defer n1 += 1;
    defer m1 += 1;
    return .{ n1, m1 };
}

pub fn main() !void {
    try std.testing.expectEqual(1, f(1));
    const n, const m = g(1, 2);
    try std.testing.expectEqual(1, n);
    try std.testing.expectEqual(2, m);
}

What's going on here? (It's a caveat)

You can generate C with zig build-exe defer.zig -ofmt=, which'll show that that the n1 += 1 applies to a separate value that doesn't get returned:

static int32_t defer_f__250(int32_t const a0) {
 int32_t t1;
 int32_t t2;
 int32_t t0;
 decl__250_39 t3;
 uint8_t t4;
 bool t5;
 /* file:2:5 */
 t0 = a0;
 /* var:n1 */
 /* file:4:5 */
 t1 = t0;
 /* file:3:11 */
 t2 = t0;
 /* file:3:14 */
 t3.f1 = zig_addo_i32(&t3.f0, t2, INT32_C(1), UINT8_C(32));
 t4 = t3.f1;
 t5 = t4 == UINT8_C(0);
 if (t5) {
  goto zig_block_0;
 }
 builtin_default_panic__315((decl__250_42){(uint8_t const *)((uint8_t const *)&__anon_1700 + (uintptr_t)0ul),(uintptr_t)16ul}, NULL, (decl__250_46){(uintptr_t)0xaaaaaaaaaaaaaaaaul,true});
 zig_unreachable();

 zig_block_0:;
 t2 = t3.f0;
 t0 = t2;
 /* file:4:5 */
 return t1;
}

Which is a little more clear in a ReleaseFast build:

static int32_t defer_f__250(int32_t const a0) {
 int32_t t1;
 int32_t t2;
 int32_t t0;
 /* file:2:5 */
 t0 = a0;
 /* var:n1 */
 /* file:4:5 */
 t1 = t0;
 /* file:3:11 */
 t2 = t0;
 /* file:3:14 */
 t2 = t2 + INT32_C(1);
 t0 = t2;
 /* file:4:5 */
 return t1;
}

reading the generated assembly with objdump

$ zig build-obj defer.zig
$ objdump -S defer.o

This produces a great deal of output, interleaved with zig source for reference. For the repeated function:

fn f(n: i32) i32 {
     7e0:       55                      push   %rbp
     7e1:       48 89 e5                mov    %rsp,%rbp
     7e4:       48 83 ec 10             sub    $0x10,%rsp
     7e8:       89 7d f8                mov    %edi,-0x8(%rbp)
    var n1 = n;
     7eb:       89 7d fc                mov    %edi,-0x4(%rbp)
    return n1;
     7ee:       8b 45 fc                mov    -0x4(%rbp),%eax
     7f1:       89 45 f0                mov    %eax,-0x10(%rbp)
    defer n1 += 1;
     7f4:       8b 45 fc                mov    -0x4(%rbp),%eax
     7f7:       ff c0                   inc    %eax
     7f9:       89 45 f4                mov    %eax,-0xc(%rbp)
     7fc:       0f 90 c0                seto   %al
     7ff:       70 02                   jo     803 <defer.f+0x23>
     801:       eb 22                   jmp    825 <defer.f+0x45>
     803:       48 bf 00 00 00 00 00    movabs $0x0,%rdi
     80a:       00 00 00 
     80d:       be 10 00 00 00          mov    $0x10,%esi
     812:       31 c0                   xor    %eax,%eax
     814:       89 c2                   mov    %eax,%edx
     816:       48 b9 00 00 00 00 00    movabs $0x0,%rcx
     81d:       00 00 00 
     820:       e8 bb 00 00 00          call   8e0 <builtin.default_panic>
     825:       8b 45 f0                mov    -0x10(%rbp),%eax
     828:       8b 4d f4                mov    -0xc(%rbp),%ecx
     82b:       89 4d fc                mov    %ecx,-0x4(%rbp)
    return n1;
     82e:       48 83 c4 10             add    $0x10,%rsp
     832:       5d                      pop    %rbp
     833:       c3                      ret
     834:       66 66 66 2e 0f 1f 84    data16 data16 cs nopw 0x0(%rax,%rax,1)
     83b:       00 00 00 00 00 

reading the LLVM IR

$ zig build-exe defer.zig -femit-llvm-ir

This produces an absolutely incredible amount of output, actually more than objdump's, including:

; Function Attrs: nounwind uwtable
define internal fastcc i32 @defer.f(i32 %0) unnamed_addr #3 !dbg !2371 {
Entry:
  %1 = alloca [4 x i8], align 4
  %2 = alloca i32, align 4
  store i32 %0, ptr %2, align 4, !dbg !2374
  call void @llvm.dbg.declare(metadata ptr %2, metadata !2375, metadata !DIExpression()), !dbg !2374
  store i32 %0, ptr %1, align 4, !dbg !2376
  call void @llvm.dbg.declare(metadata ptr %1, metadata !2377, metadata !DIExpression()), !dbg !2376
  %3 = load i32, ptr %1, align 4, !dbg !2378
  %4 = load i32, ptr %1, align 4, !dbg !2379
  %5 = call { i32, i1 } @llvm.sadd.with.overflow.i32(i32 %4, i32 1), !dbg !2380
  %6 = extractvalue { i32, i1 } %5, 1, !dbg !2380
  br i1 %6, label %OverflowFail, label %OverflowOk, !dbg !2380

OverflowFail:                                     ; preds = %Entry
  call fastcc void @builtin.default_panic(ptr @__anon_1702, i64 16, ptr null, ptr @0), !dbg !2380
  unreachable, !dbg !2380

OverflowOk:                                       ; preds = %Entry
  %7 = extractvalue { i32, i1 } %5, 0, !dbg !2380
  store i32 %7, ptr %1, align 4, !dbg !2380
  ret i32 %3, !dbg !2378
}

LLVM IR's distinct from C or asm in that it's much harder to read with high optimization.