LYNN

Fedi | Codeberg | Forgejo

Adding the TODO macro to c

The usage of TODO comments

A common use case for comments are to add notes to remind themselves at a later date to implement, or rework, some functionality of the code. These are TODO comments, and (in my usage at least) have the following structure:

/* TODO({myname}) {thing i want to do later */

An example of when this is useful is working on something but notice the API somewhere else can be improved, or is missing something that isn't critical to your current development. This is, in theory, a practical way of documenting these thoughts. It is certainly better than not documenting it at all.

So what's the issue with this method? Well, you have to go and do whatever is notated in the TODO comment for it to be useful. The only time you are going to see these TODO comments are when you are already working on something, and stumble across them in the code. This is no different than the case above, where you probably don't want to stop what you are developing or crowd your branch with unrelated enhancements.

So how can we improve workflow to better utilize these comments? I have found grep to be a very powerful tool in these situations. Consider the following code:

/* test.c */
#define EAST_DEBUG

#include "east.h"
#include <stdlib.h>

east_global const char shell_output[] = "dependency1 dependency2 dependency3";
int handle_dependency (east_string_view);
int
main (int argc, char **argv)
{
  /* TODO(lynn) Refactor this portion of code. */
  east_assert (argc < 3);
  if (argc < 2)
    {
      /* TODO(lynn) Write the usage text for this program. */
      return 1;
    }
  east_string_view sv, dep;
  sv = east_make_string (shell_output, sizeof (shell_output));
  while (sv.count > 0)
    {
      dep = east_string_next_chunk (&sv, ' ');
      printf ("%.*s\n", (int)dep.count, dep.data);
      handle_dependency (dep);
    }
  return 0;
}

int
handle_dependency (east_string_view dep)
{

  /* TODO(lynn) Handle adding dependency to the dependency graph. */
  return 0;
}

The density of TODO comments is a bit exaggerated, but I hope you can get the point. In a real life situation, this would more likely be spread out over 10-20 files and not easily parsed by the human eye.

Now we can run a grep command at the start of our workflow to check in on the TODO tasks. It generates us a bit of a TODO list that we can work through and check off:

cat grep -Rnw . -e 'TODO(lynn)'
./test.c:12:  /* TODO(lynn) Refactor this portion of code. */
./test.c:16:      /* TODO(lynn) Write the usage text for this program. */
./test.c:34:  /* TODO(lynn) Handle adding dependency to the dependency graph. */

Great! This solves the issue of stale TODO comments that never get seen again. So why is this page about a TODO macro and not about TODO comment?

A different approach to the TODO list

We have our TODO list and we're ready to get started. There's only one problem: We have file names and line numbers from our grep, but other than that the TODO comments are completely disentagled from our codepath, and are instead ordered by their appearance in the source code. This is useful if we are setting out to tackle every single TODO, but a more realistic case would be picking areas that are most relevant to what we are currently working on.

It would be nice if we could just launch our software, navigate to the piece of code we are working on, and have a list of TODO comments specific to the area we are visiting. That's where the macro comes in:

/* east.h */
#ifdef EAST_DEBUG
/* extern void __east_assert (const char *assertion, const char *__file,
                           int __line); */
extern void __east_todo (const char *message, const char *__file, int __line);
/* #define east_assert(expression)                                                \
  if (!(expression))                                                          \
    {                                                                         \
      __east_assert (#expression, __FILE__, __LINE__);                        \
    } */
#define east_todo(expression) __east_todo (#expression, __FILE__, __LINE__);
#else
/* #define east_assert(expression) ((void)(0)) */
#define east_todo(expression) ((void)(0))
#endif /* EAST_DEBUG */

I've commented out the unrelated lines of code from my library, east.h. Above we define the east_todo macro, which calls our internal library function __east_todo. It passes along the message, along with two compilers macros: __FILE__ and __LINE__, which pass along the file and the line the macro was called on, respectively. The implementation is very simple:

/* __east_todo -- push to stdout the todo reminder in the terminal color
 * yellow. This does not abort the program */
void
__east_todo (const char *message, const char *__file, int __line)
{
  fprintf (stdout, "\033[33m**TODO** %s [in:%s(%d)]\033[0m\n", message,
           __file, __line);
  fflush (stdout); /* needed for grep to function properly */
}

We do a simple print to stdout, adding some yellow colors to draw the eye. Then the code continues on as normal. If we replace the comments from our example above with this macro, it looks something like this:

/* test.c */
#define EAST_DEBUG

#include "east.h"
#include <stdlib.h>

east_global const char shell_output[] = "dependency1 dependency2 dependency3";
int handle_dependency (east_string_view);
int
main (int argc, char **argv)
{
  east_todo ("Refactor this portion of code.");
  east_assert (argc < 3);
  if (argc < 2)
    {
      east_todo ("Write the usage text for this program.");
      return 1;
    }
  east_string_view sv, dep;
  sv = east_make_string (shell_output, sizeof (shell_output));
  while (sv.count > 0)
    {
      dep = east_string_next_chunk (&sv, ' ');
      printf ("%.*s\n", (int)dep.count, dep.data);
      handle_dependency (dep);
    }
  return 0;
}

int
handle_dependency (east_string_view dep)
{

  east_todo ("Handle adding dependency to the dependency graph.");
  return 0;
}

This example is, again, quite small and meant only for displaying the capabilities. I would ask you to imagine a much larger codebase. Let's run our code now without any input:

./test | grep TODO --color=never
**TODO** "Refactor this portion of code." [in:test.c(11)]
**TODO** "Write the usage text for this program." [in:test.c(15)]

And we see here the lack of inputs should prompt a usage output to help the user understand what inputs they need to provide. The TODO comments are contextual! Let's try again, but this time passing an input:

./test 1 | grep TODO --color=never
**TODO** "Refactor this portion of code." [in:test.c(11)]
**TODO** "Handle adding dependency to the dependency graph." [in:test.c(33)]
**TODO** "Handle adding dependency to the dependency graph." [in:test.c(33)]
**TODO** "Handle adding dependency to the dependency graph." [in:test.c(33)]

And here we see just how many times that particular codepath is hit. In the future I'll add capability to build the TODO comment's string out, for example in this case adding the name of the dependency that isn't being properly handled.

Which is better?

My goal wasn't to come out and say that TODO comments are somehow obselete. They are a staple in programming culture for a reason. That being said, if you have a relatively large hobby project that has spanned a few years: run a quick grep to see how many un-finished TODO comments you have laying around. I bet you will cringe at the number.

With the macro, I hope to reduce the amount in my own projects, and maybe it will be useful to you as well.

Date: 2024-07-27 Sat 00:00

Emacs 29.4 (Org mode 9.7.11)