zig
zig copied to clipboard
Proposal: port `dbg!()` from Rust
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.
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?
It may work (especially with value: var
instead of comptime T: type, value: T
), but:
- Requires import or specifying long-ish name;
- May compile-fail for non-printable type
T
(if there are unprintable types in Zig); - 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.
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 what's anyvalue
? Is it dependent on another proposal?
@nektro what's
anyvalue
? Is it dependent on another proposal?
They probably meant anytype
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));
}
Some more thoughts on this:
-
@dbg
should compile-error if@import("builtin").mode != .Debug
. - Maybe we can remove
std.debug.print
orstd.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()
todbg
on every invocation. -
comptime @dbg
could replace@compileLog
, in which casecompile @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 (like1 + 1
orx + 1
) and then print something like1 + 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.
proof of concept https://gist.github.com/nektro/f363b7e6d72885e6d7b9a57e76c47c12
@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.