zig icon indicating copy to clipboard operation
zig copied to clipboard

Proposal: port `dbg!()` from Rust

Open vi opened this issue 4 years ago • 9 comments

Primary tool for "printf debugging" in Zig seems to be std.debug.warn. This works, but cumbersome for some use cases where you want to temporarily insert tracing of some [sub]expression without chaning the code around too much.

For example, you have this code:

const warn=@import("std").debug.warn;

fn foo(x:u64) u32 {
    var a : u64 = switch(x) {
        4 => 4,
        0...3 => 8*x + (x<<4)*3,
        5...10 => 5*x + (x<<2)*3,
        11...19 => 2*x + x*x,
        else => 0xFFFFFFFF,
    };
    return @intCast(u32, (((a*a) % 4294967291) * a) % 4294967291 );
}

pub fn main() void {
    for ([_]u64{1,4,11,14,19,23}) |x| {
        warn("{}\n", foo(x));
    }
}
175616
64
1331
2744
6859
64

Now you want to see what's the value of 2*x + x*x and how often is it called (without being bothered by other switch branches).

const warn=@import("std").debug.warn;

fn foo(x:u64) u32 {
    var a : u64 = switch(x) {
        4 => 4,
        0...3 => 8*x + (x<<4)*3,
        5...10 => 5*x + (x<<2)*3,
        11...19 => blk:{var xx=2*x + x*x; warn("QQQ {}\n",xx); break :blk x;},
        else => 0xFFFFFFFF,
    };
    return @intCast(u32, (((a*a) % 4294967291) * a) % 4294967291 );
}

pub fn main() void {
    for ([_]u64{1,4,11,14,19,23}) |x| {
        warn("{}\n", foo(x));
    }
}
175616
64
QQQ 143
1331
QQQ 224
2744
QQQ 399
6859
64

It is 10 additional things to think of (blk:, {} block, var, new identifier name xx, warn, format string that is identifiable among other output, not forgotten \n, break, :blk, final semicolon before }) just for trivial one-off debug run.

This should be nicer. Therefore I propose to adopt dbg!(x) approach from Rust.

This is what it should look like:

const warn=@import("std").debug.warn;

fn foo(x:u64) u32 {
    var a : u64 = switch(x) {
        4 => 4,
        0...3 => 8*x + (x<<4)*3,
        5...10 => 5*x + (x<<2)*3,
        11...19 => @dbg(2*x + x*x),
        else => 0xFFFFFFFF,
    };
    return @intCast(u32, (((a*a) % 4294967291) * a) % 4294967291 );
}

pub fn main() void {
    for ([_]u64{1,4,11,14,19,23}) |x| {
        warn("{}\n", foo(x));
    }
}
175616
64
[myfile.zig:8] 2*x + x*x = 143
1331
[myfile.zig:8] 2*x + x*x = 224
2744
[myfile.zig:8] 2*x + x*x = 399
6859
64
  • Filename (or filepath) is included

  • Line number

  • Citation from source code

  • Actual value, if it can ever be printed;

  • No import needed (nice to have)

  • Semantically @dbg acts like an identity function (fn dbg(x: var) @typeOf(x) { return x; })

Such function should be either dummied out (turns an into actual identity function) or just forbidden for non-debug usages (--sloppy mode only). Obviously, this rule should not be tangled with LLVM optimisation level.

See also: #2029.

vi avatar Sep 22 '19 18:09 vi

You can write this today as normal zig code:

fn dbg(comptime T: type, value: T) T {
    if (builtin.mode == builtin.Mode.Debug) {
        std.debug.warn("{}", value);
    }
    return value;
}

I think dbg(type, expr) is probably good enough. It could go in std.debug perhaps?

m-r-hunt avatar Oct 03 '19 13:10 m-r-hunt

It may work (especially with value: var instead of comptime T: type, value: T), but:

  1. Requires import or specifying long-ish name;
  2. May compile-fail for non-printable type T (if there are unprintable types in Zig);
  3. No filename:linenum information. No citation of expression.

I indent this to be a trading maintainability for quick modification-series feature, maybe available only in special mode, so ergonomy is a priority.

vi avatar Oct 03 '19 14:10 vi

Would this not be closer to the original proposal?

var warned = false;
fn dbg(value: anyvalue) @TypeOf(value) {
    std.debug.warn("[dbg]: {}", value);
    return value;
}

nektro avatar Nov 04 '20 06:11 nektro

@nektro what's anyvalue? Is it dependent on another proposal?

Mouvedia avatar Apr 15 '21 21:04 Mouvedia

@nektro what's anyvalue? Is it dependent on another proposal?

They probably meant anytype

daurnimator avatar Apr 17 '21 13:04 daurnimator

You can get the expression too and the result isn't too far from the proposal though for blocks it'd require a bit more work to get the display right.

Result:

/tmp/scratch λ zig test original.zig
[/tmp/scratch/original.zig:12]  2 * x + x * x = 143
[/tmp/scratch/example.zig:55]  @as(u32, (4 + 5) + 9) = 18
All 2 tests passed.
/tmp/scratch λ zig test example.zig
[/tmp/scratch/example.zig:55]  @as(u32, (4 + 5) + 9) = 18
All 1 tests passed.

Code (it's not pretty):

original.zig

const dbg = @import("example.zig").dbg;

test {
    _ = foo(11);
}

fn foo(x: u64) u32 {
    var a: u64 = switch (x) {
        4 => 4,
        0...3 => 8 * x + (x << 4) * 3,
        5...10 => 5 * x + (x << 2) * 3,
        11...19 => dbg(@src(), 2 * x + x * x),
        else => 0xFFFFFFFF,
    };
    return @intCast(u32, (((a * a) % 4294967291) * a) % 4294967291);
}

example.zig

const std = @import("std");
const mem = std.mem;

pub fn dbg(comptime loc: std.builtin.SourceLocation, val: anytype) @TypeOf(val) {
    var src = @embedFile(loc.file);
    var start: usize = 0;
    var line: usize = 0;

    while (mem.indexOfScalarPos(u8, src, start, '\n')) |pos| {
        line += 1;
        if (line >= loc.line) break;
        start = pos + 1;
    }

    start += loc.column - 1;

    var it = std.zig.Tokenizer.init(src[start..]);

    const code_start = start + while (true) {
        const token = it.next();
        if (token.tag == .comma) break it.index;
    } else unreachable;

    var depth: usize = 0;
    const code_end = start + while (true) {
        const token = it.next();
        switch (token.tag) {
            .r_paren => if (depth != 0) {
                depth -= 1;
            } else break it.index,
            .l_paren => depth += 1,
            else => {},
        }
    } else unreachable;

    std.debug.print("[{s}:{d}] {s} = {any}\n", .{
        loc.file,
        loc.line,
        src[code_start..code_end - 1],
        val,
    });

    return val;
}

test {
    _ = dbg(@src(), @as(u32, (4 + 5) + 9));
}

tauoverpi avatar Apr 21 '21 15:04 tauoverpi

Some more thoughts on this:

  • @dbg should compile-error if @import("builtin").mode != .Debug.
  • Maybe we can remove std.debug.print or std.log.debug in favor of @dbg. Not sure about this though.
  • I think it should be a builtin. It avoids having to import it and it would avoid having to pass @src() to dbg on every invocation.
  • comptime @dbg could replace @compileLog, in which case compile @dbg will always compile-error (and print the output) like @compileLog does currently.
  • @dbg should take varargs (args: ... like @compileLog does right now) so that we can take the code at comptime (like 1 + 1 or x + 1) and then print something like 1 + 1 = 2. I don't think it should be a tuple (e.g. @dbg(.{ 123, x + 5 })).

Here's roughly how I imagine it:

pub fn main() void {
    var x: u8 = 5;
    @dbg("hello", x + 1, 123 * 5);
}
$ zig run x.zig
src/main.zig:3: "hello" = "hello"
src/main.zig:3: x + 1   = 6
src/main.zig:3: 123 * 5 = 615

All expressions are on separate lines and all lines only of the same @dbg statement are aligned like this (notice the equals sign's alignment).

This means now we would be able to write hello worlds without the std, which might feel weird but I think it's ok because it's not a production-ready hello world anyway and you can only use it in debug mode. And of course this compile-errors in any environment without an stderr available.

wooster0 avatar Oct 16 '22 12:10 wooster0

proof of concept https://gist.github.com/nektro/f363b7e6d72885e6d7b9a57e76c47c12

nektro avatar Oct 20 '22 08:10 nektro

@nektro , This one seems to rely on stack walking and availability of symbols, so may break in embedded or exotic scenarios.

Another approach is to embed caller information into the format string itself at compile time. As far as I understand, Rust uses this approach and have special annotation for functions that would redirect Rust's analogue of @src to caller instead of the function itself.

vi avatar Oct 20 '22 12:10 vi