# $Id: Preprocessor.pm,v 1.32 2004/05/24 14:02:08 rcaputo Exp $

package POE::Preprocessor;

use strict;

use vars qw($VERSION);
$VERSION = do {my@r=(q$Revision: 1.32 $=~/\d+/g);sprintf"%d."."%04d"x$#r,@r};

use Carp qw(croak);
use Filter::Util::Call;
use Symbol qw(gensym);
use File::Path qw(mkpath);
use File::Spec;

sub MAC_PARAMETERS () { 0 }
sub MAC_CODE       () { 1 }
sub MAC_NAME       () { 2 } # only used in temporary %macro
sub MAC_FILE       () { 3 }
sub MAC_LINE       () { 4 } # only used in temporary %macro

sub STATE_PLAIN     () { 0x0000 }
sub STATE_MACRO_DEF () { 0x0001 }

sub COND_FLAG   () { 0 }
sub COND_LINE   () { 1 }
sub COND_INDENT () { 2 }

BEGIN {
  if (defined $ENV{POE_PREPROC_DUMP}) {
    eval "sub DUMP_EXPANDED () { 1 }";
    eval "sub DUMP_BASE     () { '$ENV{POE_PREPROC_DUMP}' }";
  }
  else {
    eval "sub DUMP_EXPANDED () { 0 }";
    eval "sub DUMP_BASE     () { die }";
  }
};

#sub DEBUG () { 1 }
#sub DEBUG_INVOKE () { 1 }
#sub DEBUG_DEFINE () { 1 }

#sub WARN_DEFINE  () { 1 }

BEGIN {
  defined &DEBUG        or eval 'sub DEBUG        () { 0 }'; # preprocessor
  defined &DEBUG_INVOKE or eval 'sub DEBUG_INVOKE () { 0 }'; # macro invocs
  defined &DEBUG_DEFINE or eval 'sub DEBUG_DEFINE () { 0 }'; # macro defines
  defined &WARN_DEFINE  or eval 'sub WARN_DEFINE  () { 0 }'; # redefine warning
};

### Start of regexp optimizer.

# text_trie_trie is virtually identical to code in Ilya Zakharevich's
# Text::Trie::Trie function.  The minor differences involve hardcoding
# the minimum substring length to 1 and sorting the output.

sub text_trie_trie {
  my @list = @_;
  return shift if @_ == 1;
  my (@trie, %first);

  foreach (@list) {
    my $c = substr $_, 0, 1;
    if (exists $first{$c}) {
      push @{$first{$c}}, $_;
    }
    else {
      $first{$c} = [ $_ ];
    }
  }

  foreach (sort keys %first) {
    # Find common substring
    my $substr = $first{$_}->[0];
    (push @trie, $substr), next if @{$first{$_}} == 1;
    my $l = length($substr);
    foreach (@{$first{$_}}) {
      $l-- while substr($_, 0, $l) ne substr($substr, 0, $l);
    }
    $substr = substr $substr, 0, $l;

    # Feed the trie.
    @list = map {substr $_, $l} @{$first{$_}};
    push @trie, [$substr, text_trie_trie(@list)];
  }

  @trie;
}

# This is basically Text::Trie::walkTrie, but it's hardcoded to build
# regular expressions.

sub text_trie_as_regexp {
  my @trie   = @_;
  my $num    = 0;
  my $regexp = '';

  foreach (@trie) {
    $regexp .= '|' if $num++;
    if (ref $_ eq 'ARRAY') {
      $regexp .= $_->[0] . '(?:';

      # If the first tail is empty, make the whole group optional.
      my ($tail, $first);
      if (length $_->[1]) {
        $tail  = ')';
        $first = 1;
      }
      else {
        $tail  = ')?';
        $first = 2;
      }

      # Recurse into the group of tails.
      if ($#$_ > 1) {
        $regexp .= text_trie_as_regexp( @{$_}[$first .. $#$_] );
      }
      $regexp .= $tail;
    }
    else {
      $regexp .= $_;
    }
  }

  $regexp;
}

### End of regexp optimizer.

# These must be accessible from outside the current package.
use vars qw(%conditional_stacks %excluding_code %exclude_indent);

sub fix_exclude {
  my $package_name = shift;
  $excluding_code{$package_name} = 0;
  if (@{$conditional_stacks{$package_name}}) {
    foreach my $flag (@{$conditional_stacks{$package_name}}) {
      unless ($flag->[COND_FLAG]) {
        $excluding_code{$package_name} = 1;
        $exclude_indent{$package_name} = $flag->[COND_INDENT];
        last;
      }
    }
  }
}

my (%constants, %macros, %const_regexp, %macro);

sub import {

    my $self = shift;
    my %args;
    if(@_ > 1) {
        %args = @_;
    }

    # Outer closure to define a unique scope.
    { my $macro_name = '';
      my ($macro_line, $enum_index);
      my ($package_name, $file_name, $line_number) = (caller)[0,1,2];
      my $const_regexp_dirty = 0;
      my $state = STATE_PLAIN;
      my $dump_file;

      DUMP_EXPANDED and do {
        my ($vol, $dir, $file) = File::Spec->splitpath($file_name);
        my $dest_dir      = File::Spec->catdir(DUMP_BASE, $dir);
        my $dest_filename = File::Spec->catfile($dest_dir, $file);

        mkpath($dest_dir, 0, 0777);
        $dump_file = gensym;
        open $dump_file, ">$dest_filename"
          or die "POE::Preprocessor could not create $dest_filename: $!";
        print $dump_file "package $package_name;\n";
      };

    # The following block processes inheritance requests for
    # macros/constants and enums.  added by sungo 09/2001
    my @isas;

    if($args{isa}) {
        if(ref $args{isa} eq 'ARRAY') {
            foreach my $isa (@{$args{isa}}) {
                push @isas, $isa;
            }
        } else {
            push @isas, $args{isa};
        }
        foreach my $isa (@isas) {
            eval "use $isa";
            croak "Unable to load $isa : $@" if $@;

            foreach my $const (keys %{$constants{$isa}}) {
                $constants{$package_name}->{$const} =
                  $constants{$isa}->{$const};
                $const_regexp_dirty = 1;
            }

            foreach my $macro (keys %{$macros{$isa}}) {
                $macros{$package_name}->{$macro} = $macros{$isa}->{$macro};
            }
        }
    }

    $conditional_stacks{$package_name} = [ ];
    $excluding_code{$package_name} = 0;

    my $set_const = sub {
      my ($name, $value) = @_;

      if (WARN_DEFINE && exists $constants{$package_name}->{$name}) {
        warn "const $name redefined at $file_name line $line_number\n"
          unless $constants{$package_name}->{$name} eq $value;
      }

      $constants{$package_name}->{$name} = $value;
      $const_regexp_dirty++;

      DEBUG_DEFINE and
        warn( ",-----\n",
              "| Defined a constant: $name = $value\n",
              "`-----\n"
            );
    };

    # Define the filter sub.
    filter_add
      ( sub {
          my $status = filter_read();
          $line_number++;

          ### Handle errors or EOF.
          if ($status <= 0) {
            if (@{$conditional_stacks{$package_name}}) {
              die( "include block never closed.  It probably started " .
                   "at $file_name line " .
                   $conditional_stacks{$package_name}->[0]->[COND_LINE] . "\n"
                 );
            }
            DUMP_EXPANDED and close $dump_file;
            return $status;
          }

          ### Usurp modified Perl syntax for code inclusion.  These
          ### are hardcoded and always handled.

          # Only do the conditionals if there's a flag present.
          if (/\#\s*include/) {

            # if (...) { # include
            if (/^(\s*)if\s*\((.+)\)\s*\{\s*\#\s*include\s*$/) {
              my $space = (defined $1) ? $1 : '';
              $_ =
                ( $space .
                  "BEGIN { push( \@{\$" . __PACKAGE__ .
                  "::conditional_stacks{'$package_name'}}, " .
                  "[ !!$2, $line_number, '$space' ] ); \&" . __PACKAGE__ .
                  "::fix_exclude('$package_name'); }; # $_"
                );
              s/\#\s+/\# /;

              # Dummy line in the macro.
              if ($state & STATE_MACRO_DEF) {
                local $_ = $_;
                s/B/\# B/;
                $macro_line++;
                $macro{$package_name}->[MAC_CODE] .= $_;
                DEBUG and
                  warn sprintf "%4d M: # mac 1: %s", $line_number, $_;
              }
              else {
                DEBUG and warn sprintf "%4d C: %s", $line_number, $_;
              }

              DUMP_EXPANDED and do {
                print $dump_file $_;
                close $dump_file if $status <= 0;
              };
              return $status;
            }

            # } # include
            elsif (/^\s*\}\s*\#\s*include\s*$/) {
              s/^(\s*)/$1\# /;
              pop @{$conditional_stacks{$package_name}};
              &fix_exclude($package_name);

              unless ($state & STATE_MACRO_DEF) {
                DEBUG and warn sprintf "%4d C: %s", $line_number, $_;
                DUMP_EXPANDED and do {
                  print $dump_file $_;
                  close $dump_file if $status <= 0;
                };
                return $status;
              }
            }

            # } else { # include
            elsif (/^\s*\}\s*else\s*\{\s*\#\s*include\s*$/) {
              unless (@{$conditional_stacks{$package_name}}) {
                die( "else { # include ... without if or unless " .
                     "at $file_name line $line_number\n"
                   );
                DUMP_EXPANDED and do {
                  print $dump_file $_;
                  close $dump_file;
                };
                return -1;
              }

              s/^(\s*)/$1\# /;
              $conditional_stacks{$package_name}->[-1]->[COND_FLAG] =
                !$conditional_stacks{$package_name}->[-1]->[COND_FLAG];
              &fix_exclude($package_name);

              unless ($state & STATE_MACRO_DEF) {
                DEBUG and warn sprintf "%4d C: %s", $line_number, $_;

                DUMP_EXPANDED and do {
                  print $dump_file $_;
                  close $dump_file if $status <= 0;
                };
                return $status;
              }
            }

            # unless (...) { # include
            elsif (/^(\s*)unless\s*\((.+)\)\s*\{\s*\#\s*include\s*$/) {
              my $space = (defined $1) ? $1 : '';
              $_ = ( $space .
                     "BEGIN { push( \@{\$" . __PACKAGE__ .
                     "::conditional_stacks{'$package_name'}}, " .
                     "[ !$2, $line_number, '$space' ] ); \&" . __PACKAGE__ .
                     "::fix_exclude('$package_name'); }; # $_"
                   );
              s/\#\s+/\# /;

              # Dummy line in the macro.
              if ($state & STATE_MACRO_DEF) {
                local $_ = $_;
                s/B/\# B/;
                $macro_line++;
                $macro{$package_name}->[MAC_CODE] .= $_;
                DEBUG and
                  warn sprintf "%4d M: # mac 2: %s", $line_number, $_;
              }
              else {
                DEBUG and warn sprintf "%4d C: %s", $line_number, $_;
              }

              DUMP_EXPANDED and do {
                print $dump_file $_;
                close $dump_file if $status <= 0;
              };
              return $status;
            }

            # } elsif (...) { # include
            elsif (/^(\s*)\}\s*elsif\s*\((.+)\)\s*\{\s*\#\s*include\s*$/) {
              unless (@{$conditional_stacks{$package_name}}) {
                die( "Include elsif without include if or unless " .
                     "at $file_name line $line_number\n"
                   );
                DUMP_EXPANDED and do {
                  print $dump_file $_;
                  close $dump_file;
                };
                return -1;
              }

              my $space = (defined $1) ? $1 : '';
              $_ = ( $space .
                     "BEGIN { \$" . __PACKAGE__ .
                     "::conditional_stacks{'$package_name'}->[-1] = " .
                     "[ !!$2, $line_number, '$space' ]; \&" . __PACKAGE__ .
                     "::fix_exclude('$package_name'); }; # $_"
                   );
              s/\#\s+/\# /;

              # Dummy line in the macro.
              if ($state & STATE_MACRO_DEF) {
                local $_ = $_;
                s/B/\# B/;
                $macro_line++;
                $macro{$package_name}->[MAC_CODE] .= $_;
                DEBUG and
                  warn sprintf "%4d M: # mac 3: %s", $line_number, $_;
              }
              else {
                DEBUG and warn sprintf "%4d C: %s", $line_number, $_;
              }

              DUMP_EXPANDED and do {
                print $dump_file $_;
                close $dump_file if $status <= 0;
              };
              return $status;
            }
          }

          ### Not including code, so comment it out.  Don't return
          ### $status here since the code may well be in a macro.
          if ($excluding_code{$package_name}) {
            s{^($exclude_indent{$package_name})?}
             {$exclude_indent{$package_name}\# };

            # Kludge: Must thwart macros on this line.
            s/\{\%(.*?)\%\}/MACRO($1)/g;

            unless ($state & STATE_MACRO_DEF) {
              DEBUG and warn sprintf "%4d C: %s", $line_number, $_;
              DUMP_EXPANDED and do {
                print $dump_file $_;
                close $dump_file if $status <= 0;
              };
              return $status;
            }
          }

          ### Inside a macro definition.
          if ($state & STATE_MACRO_DEF) {

            # Close it!
            if (/^\}\s*$/) {
              $state = STATE_PLAIN;

              DEBUG_DEFINE and
                warn
                  ( ",-----\n",
                    "| Defined macro $macro_name\n",
                    "| Parameters: ",
                    @{$macro{$package_name}->[MAC_PARAMETERS]}, "\n",
                    "| Code: {\n",
                    $macro{$package_name}->[MAC_CODE],
                    "| }\n",
                    "`-----\n"
                  );

              $macro{$package_name}->[MAC_CODE] =~ s/^\s*//;
              $macro{$package_name}->[MAC_CODE] =~ s/\s*$//;

              if ( WARN_DEFINE &&
                   exists $macros{$package_name}->{$macro_name}
                 ) {
                warn( "macro $macro_name redefined at ",
                      "$file_name line $line_number\n"
                    )
                  if ( $macros{$package_name}->{$macro_name}->[MAC_CODE] ne
                       $macro{$package_name}->[MAC_CODE]
                     );
              }

              $macros{$package_name}->{$macro_name} = $macro{$package_name};

              $macro_name = '';
            }

            # Otherwise append this line to the macro.
            else {
              $macro_line++;
              $macro{$package_name}->[MAC_CODE] .= $_;
            }

            # Either way, the code must not go on.
            $_ = "# mac 4: $_";
            DEBUG and warn sprintf "%4d M: %s", $line_number, $_;

            DUMP_EXPANDED and do {
              print $dump_file $_;
              close $dump_file if $status <= 0;
            };
            return $status;
          }

          ### Ignore everything after __END__ or __DATA__.  This works
          ### around a coredump in 5.005_61 through 5.6.0 at the
          ### expense of preprocessing data and documentation.
          if (/^__(END|DATA)__\s*$/) {
            $_ = "# $_";
            DUMP_EXPANDED and do {
              print $dump_file $_;
              close $dump_file;
            };
            return 0;
          }

          ### We're done if we're excluding code.
          if ($excluding_code{$package_name}) {
            DUMP_EXPANDED and do {
              print $dump_file $_;
              close $dump_file if $status <= 0;
            };
            return $status;
          }

          ### Define an enum.
          if (/^enum(?:\s+(\d+|\+))?\s+(.*?)\s*$/) {
            my $temp_line = $_;

            $enum_index = ( (defined $1)
                            ? ( ($1 eq '+')
                                ? $enum_index
                                : $1
                              )
                            : 0
                          );
            foreach (split /\s+/, $2) {
              &{$set_const}($_, $enum_index++);
            }

            $_ = "# $temp_line";

            DEBUG and warn sprintf "%4d E: %s", $line_number, $_;

            DUMP_EXPANDED and do {
              print $dump_file $_;
              close $dump_file if $status <= 0;
            };
            return $status;
          }

          ### Define a constant.
          if (/^const\s+(\S+)\s+(.+?)\s*$/i) {
            &{$set_const}($1, $2);
            $_ = "# $_";
            DEBUG and warn sprintf "%4d E: %s", $line_number, $_;

            DUMP_EXPANDED and do {
              print $dump_file $_;
              close $dump_file if $status <= 0;
            };
            return $status;
          }

          ### Begin a macro definition.
          if (/^macro\s*(\w+)\s*(?:\((.*?)\))?\s*\{\s*$/) {
            $state = STATE_MACRO_DEF;

            my $temp_line = $_;

            $macro_name = $1;
            $macro_line = 0;
            my @macro_params =
              ( (defined $2)
                ? split(/\s*\,\s*/, $2)
                : ()
              );

            $macro{$package_name} =
              [ \@macro_params, # MAC_PARAMETERS
                '',             # MAC_CODE
                $macro_name,    # MAC_NAME
                $file_name,     # MAC_FILE
                $line_number,   # MAC_LINE
              ];

            $_ = "# $temp_line";
            DEBUG and warn sprintf "%4d D: %s", $line_number, $_;

            DUMP_EXPANDED and do {
              print $dump_file $_;
              close $dump_file if $status <= 0;
            };
            return $status;
          }

          ### Perform macro substitutions.
          my $substitutions = 0;
          while (/(\{\%\s+(\S+)\s*(.*?)\s*\%\})/gs) {
            my ($name, $params) = ($2, $3);

            # Backtrack to the beginning of the substitution so that
            # the newly inserted text may also be checked.
            pos($_) -= length($1);

            DEBUG_INVOKE and
              warn ",-----\n| macro invocation: $name $params\n";

            if (exists $macros{$package_name}->{$name}) {

              my @use_params = split /\s*\,\s*/, $params;
              my @mac_params =
                @{$macros{$package_name}->{$name}->[MAC_PARAMETERS]};

              if (@use_params != @mac_params) {
                warn( "macro $name paramter count (",
                      scalar(@use_params),
                      ") doesn't match defined count (",
                      scalar(@mac_params),
                      ") at $file_name line $line_number\n"
                    );

                DUMP_EXPANDED and do {
                  print $dump_file $_;
                  close $dump_file if $status <= 0;
                };
                return $status;
              }

              # Build a new bit of code here.
              my $substitution = $macros{$package_name}->{$name}->[MAC_CODE];
              my $macro_file   = $macros{$package_name}->{$name}->[MAC_FILE];
              my $macro_line   = $macros{$package_name}->{$name}->[MAC_LINE];

              foreach my $mac_param (@mac_params) {
                my $use_param = shift @use_params;
                1 while ($substitution =~ s/$mac_param/$use_param/g);
              }

              unless ($^P) {
                my @sub_lines = split /\n/, $substitution;
                my $sub_line = @sub_lines;
                while ($sub_line--) {
                  splice( @sub_lines, $sub_line, 0,
                          "# line $line_number " .
                          "\"macro $name (defined in $macro_file at line " .
                          ($macro_line + $sub_line + 1) . ") " .
                          "invoked from $file_name\""
                        );
                }
                $substitution = join "\n", @sub_lines;
              }

              substr($_, pos($_), length($1)) = $substitution;
              $_ .= "# line " . ($line_number+1) . " \"$file_name\"\n"
                unless $^P;

              DEBUG_INVOKE and warn "$_`-----\n";

              $substitutions++;
            }
            else {
              die( "macro $name has not been defined ",
                   "at $file_name line $line_number\n"
                 );
              last;
            }
          }

          # Only rebuild the constant regexp if necessary.  This
          # prevents redundant regexp rebuilds when defining several
          # constants all together.
          if ($const_regexp_dirty) {
            $const_regexp{$package_name} =
              text_trie_as_regexp
                ( text_trie_trie(keys %{$constants{$package_name}})
                );
            $const_regexp_dirty = 0;
          }

          # Perform constant substitutions.
          if (defined $const_regexp{$package_name}) {
            $substitutions +=
              s[\b($const_regexp{$package_name})\b]
               [$constants{$package_name}->{$1}]sg;
          }

          # Trace substitutions.
          if (DEBUG) {
            if ($substitutions) {
              foreach my $line (split /\n/) {
                warn sprintf "%4d S: %s\n", $line_number, $line;
              }
            }
            else {
              warn sprintf "%4d |: %s", $line_number, $_;
            }
          }

          DUMP_EXPANDED and do {
            print $dump_file $_;
            close $dump_file if $status <= 0;
          };
          return $status;
        }
      );
  }
}

# Clear a package's macros.  Used for destructive testing.
sub clear_package {
  my ($self, $package) = @_;
  delete $constants{$package};
  delete $macros{$package};
  delete $const_regexp{$package};
  delete $macro{$package};
}

1;

__END__

=head1 NAME

POE::Preprocessor - a macro/const/enum preprocessor

=head1 SYNOPSIS

  use POE::Preprocessor;

  # use POE::Preprocessor ( isa => 'POE::SomeModule' );

  macro max (one,two) {
    ((one) > (two) ? (one) : (two))
  }

  print {% max $one, $two %}, "\n";

  const PI 3.14159265359

  print "PI\n";  # Substitutions don't grok Perl!

  enum ZERO ONE TWO
  enum 12 TWELVE THIRTEEN FOURTEEN
  enum + FIFTEEN SIXTEEN SEVENTEEN

  print "ZERO ONE TWO TWELVE THIRTEEN FOURTEEN FIFTEEN SIXTEEN SEVENTEEN\n";

  if ($expression) {      # include
     ... lines of code ...
  }                       # include

  unless ($expression) {  # include
    ... lines of code ...
  } elsif ($expression) { # include
    ... lines of code ...
  } else {                # include
    ... lines of code ...
  }                       # include

=head1 DESCRIPTION

POE::Preprocessor is a Perl source filter that implements a simple
macro substitution language.  Think of it like compile-time code
templates.

=head2 Macros

Macros are defined with the C<macro> statement.  The syntax is similar
to Perl subs:

  macro macro_name (parameter_0, parameter_1) {
    macro code ... parameter_0 ... parameter_1 ...
  }

The open brace is required to be on the same line as the C<macro>
statement.  The Preprocessor doesn't analyze macro bodies.  Instead,
it assumes that any closing brace in the leftmost column ends an open
macro.

The parameter list is optional for macros that don't accept
parameters.

  macro macro_name {
    macro code;
  }

Macros are substituted into a program with a syntax borrowed from
Iaijutsu and altered slightly to jive with Perl's native syntax.

  {% macro_name $param_1, 'param two' %}

This is the code the first macro would generate:

  macro code ... $param_1 ... 'param two' ...

It's very simplistic.  See POE::Kernel for extensive macro use.

=head2 Constants and Enumerations

The C<const> command defines a constant.

  const CONSTANT_NAME    'constant value'
  const ANOTHER_CONSTANT 23

Enumerations are defined with the C<emun> command.  Enumerations start
from zero by default:

  enum ZEROTH FIRST SECOND ...

If the first parameter of an enumeration is a number, then the
enumerated constants will start with that value:

  enum 10 TENTH ELEVENTH TWELFTH

C<enum> statements may not span lines.  If the first enumeration
parameter is a plus sign, the constants will start where a previous
C<enum> left off.

  enum 13 THIRTEENTH FOURTEENTH  FIFTEENTH
  enum +  SIXTEENTH  SEVENTEENTH EIGHTEENTH

=head2 Conditional Code Inclusion (#ifdef)

The preprocessor supports something like cpp's #if/#else/#endif by
usurping a bit of Perl's conditional syntax.  The following
conditional statements will be evaluated at compile time if they are
followed by the comment C<# include>:

  if (EXPRESSION) {      # include
    BLOCK;
  } elsif (EXPRESSION) { # include
    BLOCK;
  } else {               # include
    BLOCK;
  }                      # include

  unless (EXPRESSION) {  # include
    BLOCK;
  }                      # include

The code in each conditional statement's BLOCK will be included or
excluded in the compiled code depending on the outcome of its
EXPRESSION.

Conditional includes are nestable, but else and elsif must be on the
same line as the previous block's closing brace, as they are in the
previous example.

Conditional includes are experimental pending a decision on how useful
they are.

=head1 IMPORTING MACROS/CONSTANTS

    use POE::Preprocessor ( isa => 'POE::SomeModule' );

This method of calling Preprocessor causes the macros and constants of
C<POE::SomeModule> to be imported for use in the current namespace.
These macros and constants can be overriden simply by defining items
in the current namespace of the same name.

Note: if the macros in C<POE::SomeModule> require additional perl
modules, any code which imports these macros will need to C<use>
those modules as well.

=head1 DEBUGGING

POE::Preprocessor has three debugging constants which may be defined
before the first time POE::Preprocessor is used.

To trace source filtering in general, and to see the resulting code
and operations performed on each line:

  sub POE::Preprocessor::DEBUG () { 1 }

To trace macro invocations as they happen:

  sub POE::Preprocessor::DEBUG_INVOKE () { 1 }

To see macro, constant, and enum definitions:

  sub POE::Preprocessor::DEBUG_DEFINE () { 1 }

To see warnings when a macro or constant is redefined:

  sub POE::Preprocessor::WARN_DEFINE () { 1 }

=head1 ENVIRONMENT

Setting the POE_PREPROC_DUMP environment variable causes
POE::Preprocessor to write new copies of the modules it expands.
POE_PREPROC_DUMP should contain the name of a base directory.  It
need not exist.  All the expanded files will be written under the
directory in POE_PREPROC_DUMP.

Note: Some modules such as POE::Kernel alter the macros they load at
compile time.  Versions of these modules created by POE_PREPROC_DUMP
may only be used in the same situation they were created in.  For
instance, a version of POE::Kernel created with Gtk support macros may
only be used in Gtk programs from that point on.

Note: Because POE::Preprocessor can only dump source code from the
point it it is used in a file, it should be the first statement after
C<package Foo;> in a module.  Otherwise code before it will be missing
from the resulting expanded file.  POE::Preprocessor will create a new
C<package Foo;> line to replace the one that it could not see.

This is good:

  package Foo;  # Not seen but simulated in the expanded version.
  use POE::Preprocessor;
  use Carp;     # Seen and included in the expanded version.

This is bad:

  package Foo;  # Not seen but simulated in the expanded version.
  use Carp;     # Not seen and OMITTED FROM the expanded version.
  use POE::Preprocessor;

=head1 PERLAPP, PERL2EXE, AND PAR SUPPORT

PerlApp, perl2exe, and PAR are program archivers, similar to Java's
JAR.  These utilities bundle your program with its dependencies and
perhaps a perl executable.  The result is a single, large file that
may be more easily deployable than the usual Perl program.

The archivers find dependencies by looking for C<use> statements.  POE
does a lot of dynamic loading at startup, so its C<use> statements are
not always detectable.

To work around the issue, add C<use> statements for modules that are
missing from your bundled distribution.

Things to look for:

PerlApp cannot find modules imported using the L<POE> module.  That
is, this will not work:

  use POE qw(A B C);

This is the working equivalent:

  # Dynamically required by POE::Kernel.
  use POE::Resource::Aliases;
  use POE::Resource::Events;
  use POE::Resource::Extrefs;
  use POE::Resource::FileHandles;
  use POE::Resource::SIDs;
  use POE::Resource::Sessions;
  use POE::Resource::Signals;
  use POE::Resource::Statistics;

  # Dynamically required by POE.pm.
  use POE::Kernel;
  use POE::Session;

  use POE::A;
  use POE::B;
  use POE::C;

PerlApp does not support POE::Processor or any other source filter, so
it's necessary to generate a static version of the files to be
included.  Setting the C<POE_PREPROC_DUMP> environment variable will
cause POE::Preprocessor to dump a processed version of the file.

  set POE_PREPROC_DUMP=c:\rocco\preproc

Run the program.  As the program is run, its preprocessed files will
be placed in subdirectories under L<POE_PREPROC_DUMP>.

  perl MyPoeApp.perl

The preprocessed files must now be found, and their parent directory
must be placed in the PERL5LIB environment variable.

  C:\rocco\POE-0.20>dir /b /a:d /s c:\rocco\preproc

Might show these directories:

  C:\rocco\preproc\POE
  C:\rocco\preproc\POE\Kernel

The POE modules have been dumped into "C:\rocco\preproc\POE" and its
subdirectories.  For C<use POE::Foo> to find POE/Foo.pm, the
"c:\rocco\preproc" directory must be prepended to PERL5LIB.

  set PERL5LIB=c:\rocco\preproc

PerlApp can finally build an EXE version of the program.

  perlapp --exe MyPoeApp.exe --clean MyPoeApp.perl

MyPoeApp.exe should now be a stand-alone executable version of your
Perl program.

Thanks to...

Zoltan Kandi for testing and documenting this.

Lance Braswell for pointing out the POE::Resource classes need to be
loaded.

=head1 BUGS

Source filters are line-based, and so is the macro language.  The only
constructs that may span lines are macro definitions, and those *must*
span lines.

The regular expressions that detect and replace code are simplistic
and may not do the right things when given challenging Perl syntax to
parse.  For example, constants are replaced within strings.

Substitution is done in two phases: macros first, then constants.  It
would be nicer (and more dangerous) if the phases looped around and
around until no more substitutions occurred.

The regexp builder makes silly subexpressions like /(?:|m)/.  That
could be done better as /m?/ or /(?:jklm)?/ if the literal is longer
than a single character.

The "# include" directives are not compatible with the
POE_PREPROC_DUMP environment variable.

=head1 SEE ALSO

The regexp optimizer is based on code in Ilya Zakharevich's
Text::Trie.

=head1 AUTHOR & COPYRIGHT

POE::Preprocessor is Copyright 2000 Rocco Caputo.  Some parts are
Copyright 2001 Matt Cashner. All rights reserved.  POE::Preprocessor
is free software; you may redistribute it and/or modify it under
the same terms as Perl itself.

=cut
