Friday, 12 July 2013

A Look at Conditional Compiling of Diagnostics

From time to time, I add diagnostic messages to be printed by my code so I can track what the program is doing. I often wondered if there was a way to get Perl to conditionally compile these statements and to get it to do so automatically. Here's how:

Conditional Compiling

Perl conditional compiles code if the condition of an if statement is constant.

Here is an example of the print being compiled out:

  $ perl -e'if( 0 ) { print"Hello\n"; }'

We can see what is actually happening with Deparse:

  $ perl -MO=Deparse -e'if( 0 ) { print"Hello\n"; }'
  '???';
  -e syntax OK

Perl replaces the if statement with '???';, which does nothing. If we can the 0 in the if to a 1:

  $ perl -MO=Deparse -e'if( 1 ) { print"Hello\n"; }'
  do {
      print "Hello\n"
  };
  -e syntax OK

The if statement is replaced with a do.

But we don't want to have to go thru all the code and replace 0s with 1s to see the diagnostics and back again to get rid of them. Is there a way to do this from one place? Yes, there is.

Inline Subroutines

Perl will in-line subroutines if they are prototyped with no parameters and consist of only a constant. This, for example:

  $ perl -MO=Deparse -e'sub hello () { "Hello"; }; print hello, "\n";'
  sub hello () { 'Hello' }
  print 'Hello', "\n";
  -e syntax OK

The subroutine call gets replaced with its contents, in this case, the string, 'Hello'.

If fact, that's how the pragmatic, constant works:

  perl -MO=Deparse -e'use constant hello => "Hello"; print hello, "\n";'
  use constant ('hello', 'Hello');
  print 'Hello', "\n";
  -e syntax OK

Conditional Diagnostics

Putting what we learned above into a script, we can have conditional diagnostics:

  #!/usr/bin/env perl

  use strict;
  use warnings;

  use constant _DIAGNOSTIC => 0;

  if( _DIAGNOSTIC ){
      print "hello\n";
  }

Running it thru Deparse shows:

  $ perl -MO=Deparse myscript
  use constant ('_DIAGNOSTIC', 0);
  use warnings;
  use strict;
  '???';
  myscript syntax OK

And changing the 0 to 1:

  $ perl -MO=Deparse myscript
  use constant ('_DIAGNOSTIC', 1);
  use warnings;
  use strict;
  do {
      print "hello\n"
  };
  myscript syntax OK

So, now we can control the diagnostics messages from one place. But this is still not good enough. The problem being is that you have to remember to change all the 1s back to 0s before you put the code into production, and you know that isn't always going to happen. Is there a simple way to toggle diagnostics externally? Well, yes there is.

It's All in the Name

Change myscript to this:

  #!/usr/bin/env perl

  use strict;
  use warnings;

  use constant _DIAGNOSTIC => $0 =~ /_D(?:\.pl)?$/ ? 1 : 0;

  if( _DIAGNOSTIC ){
      print "hello\n";
  }

The complex expression for _DIAGNOSITC is the name of the script contained in $0 which is matched to a _D (with an optional .pl) at the end of its name. If it is, then 1 is used as the constant, else 0.

Now create a symbolic link in the directory:

  $ ln -s ./myscript myscript_D

The results of Deparse of myscript:

  $ perl -MO=Deparse myscript
  use constant ('_DIAGNOSTIC', $0 =~ /_D(?:\.pl)?$/ ? 1 : 0);
  use warnings;
  use strict;
  '???';
  myscript syntax OK

And of myscript_D:

  $ perl -MO=Deparse myscript_D 
  use constant ('_DIAGNOSTIC', $0 =~ /_D(?:\.pl)?$/ ? 1 : 0);
  use warnings;
  use strict;
  do {
      print "hello\n"
  };
  myscript_D syntax OK

Summary

With knowledge of Perl's workings, as discovered using Deparse, it is possible to create conditionally compiled diagnostics that don't require constant editing of the code. And always having a non-diagnostic, production quality code in your code.

Update

Thanks to luben for a better way to invoke the diagnostics via an environment variable. myscript becomes:

  #!/usr/bin/env perl

  use strict;
  use warnings;

  use constant DEBUG = $ENV{DEBUG};

  if( DEBUG ){
      print "hello\n";
  }

To run with the diagnostics:

DEBUG=1 myscript