13 hours ago

A while back (perhaps my first D forum post) I asked about implementing an equivalent of other languages' very useful debug printing macros such as Julia's @show or Nim's dump or Rust's dbg!. Someone on the forum here very helpfully showed me how to do it via D's mixin feature. I have since then refined my own tiny module for doing so, with a few more features. Here is the code for it, in my trace.d module:

public import std.conv;

/// `trace_prefix` provides the common `module_name.function_name-line_number:\t` prefix that is added to each of the tracing functions in this module. The other functions are likely to be more useful, unless a new function is being created that reuses the same trace prefix format.
string trace_prefix(const bool should_mark = false) {
	return `__FUNCTION__ ~ "-" ~ to!string(__LINE__) ~ ` ~ (should_mark ? `"*"` : `""`) ~ `~ ": \t"`;
}

/// `trace_message` outputs any arbitrary string you pass it, but prefixed by an indicator of what module, function, and line number the trace_message is located on, thus aiding print-based debugging.
string trace_message(string message) {
	return `writeln(` ~ trace_prefix() ~ ` ~ q"<` ~ message ~ `>");`;
}

/// `show` eases print debugging by printing both an expression as a string and its corresponding evaluated value. It is intended to be used via `mixin`, such as in `mixin(show("1 + 2")`, which will print something like `module.function-line: 1 + 2 == 3`. The `module.function-line:` prefix helps distinguish debug printing from normal output and also helps to track where output is coming from.
///
/// Printing both an expression's code and the expression's value makes print debugging easier to keep track of accurately (especially when multiple print statements are used, which would otherwise be more easily confused with each other) and less redundant (since `show` being a macro prevents the user from needing to write the expression twice).
///
/// However, assertions (via `assert`) and unit tests (via `unittest`) are often a better way to debug and maintain code than any form of print debugging, because (1) assertions are enforced automatically instead of requiring manual inspection and (2) reading through print statements consumes a lot of time in aggregate whereas assertions are instantaneous and hence much more expedient when well applicable. Nonetheless, print debugging is still very useful and intuitive, especially when you don't know what a value is at all.
string show(string expr) {
  return `writefln(` ~ trace_prefix() ~ `~ "%s == %s", q"<` ~ expr ~ `>", ` ~ expr ~ `);`;
}

/// `trace_scope` returns a mixin string that if mixed in at the beginning of a scope inserts code that logs when the current scope starts and ends, which may be useful for debugging potentially, since it informs of when you are actually inside that scope or not, which may or may not be when you think. Use it by writing `mixin(trace_scope);`, placing it at the top of the scope you intend to trace.
string trace_scope(string scope_name) {
	return
		`writeln(` ~ trace_prefix() ~ ` ~ q"<` ~ scope_name ~ `>" ~ " entered");` ~
		`scope(exit) writeln(` ~ trace_prefix(true) ~ ` ~ q"<` ~ scope_name ~ `>" ~ " exited");`;
}

show is for examining the values of things in a clean and organized way (the most common use case).

trace_message is for any arbitrary string message tagged with the module, function, and line.

trace_scope is for easier printing of when any specific scope starts and ends.

All are useful.

You use them by writing mixin(trace_function_name(...)) and such, since they all return strings and you have to use mixin to apply arbitrary macros in D. For example, try mixin(show(1 + 2)) or mixin(trace_scope(__FUNCTION__)) or similar.

(Unimportantly, you can see that I'm using snake_case, though I've debated on and off using D's camelCase but I've always liked snake_case and PascalCase and other conventions more than camelCase in languages.)

Anyway, I really like how useful this is and basically this kind of print debugging feature should be built in to all modern languages in my opinion (and indeed is in all of the most popular ones in some form and for good reason).

It works fine, but I was wondering what suggestions the D community may have regarding both (1) whether or not better pre-built functionality for this exists in the D standard library and community libraries already and (2) how the above code could be improved to be more readable. I tried to use string format but it didn't even work right when I tried it.

How would you refactor it if you were trying to make it more polished and readable?

D's string-based mixins that support arbitrary string code are useful, but as you can see keeping the quotations delimiters and concatenation operations straight above is not very easy and it took a long time to get these tracing functions working relative to their conceptual simplicity because strings were so subtle and easy to confuse oneself with the quote delimiters.

I also figured that other beginners could benefit from the above.

Such tracing functions provide a far better way of print debugging than just raw print statements, although assertions and unit tests are better in ideal cases because they don't require manual examination, but print debugging is useful for when you don't know what the state of something actually is at all of course.

What are your thoughts, if any?

(PS: Thanks for reading.)

50 minutes ago

Oh, and here's another minor update. I've added another public import line to the module to ensure it actually works correctly when imported:

public import std.stdio;

Previously I had carelessly omitted this because I was using trace.d only in contexts where std.stdio had already been imported, but clearly that would need remedied in the general use case.

The public imports make the imports visible in the module of any module that imports trace.d, whereas otherwise such necessary dependencies for use wouldn't be visible.

Text based D macros (string mixins) can't know their dependencies, I'd imagine, hence the reason for adding the two imports (of std.stdio and std.conv).