Author: allison
Date: Thu Jan  1 12:16:59 2009
New Revision: 34762

Modified:
   trunk/lib/Parrot/Configure/Compiler.pm
   trunk/t/steps/gen_makefiles-01.t

Log:
[pdd30install] Add new conditioned line syntax #IF, #UNLESS, #ELSIF, and #ELSE.
Extracted from pdd30install branch (Reini Urban), plus additional cleanups from
code/doc review.


Modified: trunk/lib/Parrot/Configure/Compiler.pm
==============================================================================
--- trunk/lib/Parrot/Configure/Compiler.pm      (original)
+++ trunk/lib/Parrot/Configure/Compiler.pm      Thu Jan  1 12:16:59 2009
@@ -210,16 +210,64 @@
 Its value will be detected automatically by target file name unless you set
 it to a special value C<none>.
 
-=item conditioned_lines
+=item conditioned_lines #IF #UNLESS #ELSIF #ELSE
 
-If C<conditioned_lines> is true, then lines in the file that begin with:
-C<#CONDITIONED_LINE(var):> are skipped if the C<var> condition is false. Lines
-that begin with C<#INVERSE_CONDITIONED_LINE(var):> are skipped if
-the C<var> condition is true.  For instance:
+If conditioned_lines is true, then lines beginning in #IF, #UNLESS, #ELSIF, and
+#ELSE are evaluated conditionally, and the content after the C<:> is included
+or excluded, dependending on the evaluation of the expression.
 
-  #CONDITIONED_LINE(win32): $(SRC_DIR)/atomic/gcc_x86$(O)
+Lines beginning with C<#IF(expr):> are skipped if the expr condition is false,
+otherwise the content after the C<:> is inserted. Lines beginning with
+C<#UNLESS(expr):> are skipped if the expr condition is true, otherwise the
+content after the C<:> is inserted. Lines beginning with C<#ELSIF(expr):> or
+C<#ELSE:> are evaluated if the preceding C<#IF(expr):> evaluated to false.
 
-will be processed if the platform is win32.
+A condition expr may be:
+
+  * A single key, which is true if a config key is true,
+  * Equal to the platform name or the osname - case-sensitive,
+  * A C<key==value> expression, which is trun if the config key has the
+    expected value, or
+  * A logical combination of C<|>, C<OR>, C<&>, C<AND>, C<!>, C<NOT>.
+
+A key must only consist of the characters A-Z a-z 0-9 _ -, and is checked
+case-sensitively against the configuration key or the platform name. Truth is
+defined as any value that is not 0, an empty string, or C<undef>.
+
+The value in C<key==value> expressions may not contain spaces. Quotes in
+values are not supported.
+
+The word ops C<AND>, C<OR> and C<NOT> are case-insensitive. C<!> and C<NOT>
+bind closer than C<&>, C<AND>, C<|>, and C<OR>. The order of precedence for
+C<AND> and C<OR> is undefined.
+
+
+For instance:
+
+  #IF(win32): $(SRC_DIR)/atomic/gcc_x86$(O)
+
+will be included if the platform is win32.
+
+  #IF(cpuarch==i386): $(SRC_DIR)/atomic/gcc_x86$(O)
+
+will be included if the value of the config key "cpuarch" is "i386".
+
+  #IF(cpuarch==i386): $(SRC_DIR)/atomic/gcc_x86$(O)
+  #ELSIF(cpuarch==sparcv9): $(SRC_DIR)/atomic/sparc_v9.s
+  #ELSE:
+
+will include " $(SRC_DIR)/atomic/gcc_x86$(O)" if the config key "cpuarch" is
+ste to "i386", will include " $(SRC_DIR)/atomic/sparc_v9.s" instead if
+"cpuarch" is set to "sparcv9", and will include an empty line otherwise.
+
+  #IF(win32 and glut and not cygwin):
+
+will be used on "win32" and if "glut" is defined, but not on "cygwin".
+
+B<Legacy Syntax:>
+
+The old syntax #CONDITIONED_LINE(var): and
+#INVERSE_CONDITIONED_LINE(var): is still supported, but is deprecated.
 
 =item comment_type
 
@@ -360,6 +408,8 @@
     # this loop can not be implemented as a foreach loop as the body
     # is dependent on <IN> being evaluated lazily
 
+    my $former_truth = -1;
+  LINE:
     while ( my $line = <$in> ) {
 
         # everything after the line starting with #perl is eval'ed
@@ -376,16 +426,46 @@
             $text =~ s{ \@ (\w+) \@ }{\$conf->data->get("$1")}gx;
             eval $text;
             die $@ if $@;
-            last;
+            last LINE;
         }
         if ( $options{conditioned_lines} ) {
-            if ( $line =~ m/^#CONDITIONED_LINE\(([^)]+)\):(.*)/s ) {
-                next unless $conf->data->get($1);
-                $line = $2;
-            }
-            elsif ( $line =~ m/^#INVERSE_CONDITIONED_LINE\(([^)]+)\):(.*)/s ) {
-                next if $conf->data->get($1);
-                $line = $2;
+            my ($op, $expr, $rest);
+            # allow multiple keys and nested parens here
+            if (($op,$expr,$rest)=($line =~ 
m/^#(IF|UNLESS|ELSIF)\((.+)\):(.*)/s)) {
+                if (($op eq 'ELSIF') and $former_truth) {
+                    next LINE;  # no useless check if former IF was true
+                }
+                my $truth = cond_eval($conf, $expr);
+                if ($op eq 'IF') {
+                    $former_truth = $truth;
+                    next LINE unless $truth;
+                }
+                elsif ($op eq 'UNLESS') {
+                    $former_truth = !$truth;
+                    next LINE if $truth;
+                }
+                elsif ($op eq 'ELSIF') {
+                    $former_truth = $truth;
+                    next LINE unless $truth;
+                }
+                $line = $rest;
+            }
+            elsif ( $former_truth != -1 and $line =~ m/^#ELSE:(.*)/s ) {
+                next LINE if $former_truth;
+                $line = $1;
+            }
+            # Legacy, DEPRECATED.
+            elsif (($expr,$rest)=($line =~ 
m/^#CONDITIONED_LINE\(([^)]+)\):(.*)/s)) {
+                next LINE unless cond_eval($conf, $expr);
+                $line = $rest;
+            }
+            elsif (($expr,$rest)=($line =~ 
m/^#INVERSE_CONDITIONED_LINE\(([^)]+)\):(.*)/s )) {
+                next LINE if cond_eval($conf, $expr);
+                $line = $rest;
+            }
+
+            else { # reset
+                $former_truth = -1; # ELSE must immediately follow a 
conditional.
             }
         }
 
@@ -485,6 +565,123 @@
     move_if_diff( "$target.tmp", $target, $options{ignore_pattern} );
 }
 
+# Return the next subexpression from the expression in $_[0]
+# and remove it from the input expression.
+# Allowed chars: A-Z a-z 0-9 _ -, so let's take [-\w].
+# E.g. "(not win32 and has_glut)"
+#        => not win32 => has_glut
+#      "(!win32&has_glut)|cygwin"   - perl-style
+#        !win32&has_glut => !win32 => &has_glut => |cygwin
+sub next_expr {
+    my $s = $_[0];
+    return "" unless $s;
+    # start of a subexpression?
+    if ($s =~ /^\((.+)\)\s*(.*)/o) {    # longest match to matching closing 
paren
+        $_[0] = $2 ? $2 : "";           # modify the 2nd arg
+        return $1;
+    }
+    else {
+        $s =~ s/^\s+//;                 # left-trim to make it more robust
+        if ($s =~ m/^([-\w=]+)\s*(.*)?/o) { # shortest match to next non-word 
char
+            # start with word expr
+            $_[0] = $2 ? $2 : "";       # modify the 2nd arg expr in the caller
+            return $1;
+        }
+        else {
+            # special case: start with non-word op (perl-syntax only)
+            $s =~ m/^([|&!])\s*(.*)?/o; # shortest match to next word char
+            $_[0] = $2 ? $2 : "";       # modify the 2nd arg expr in the caller
+            return $1;
+        }
+    }
+}
+
+# Checks the logical truth of the hash value: exists and not empty.
+# Also check the platform name, the 'osname' key, if the hash key does not 
exist.
+# Also check for key==value, like #IF(ld==gcc)
+sub cond_eval_single {
+    my $conf = $_[0];
+    my $key  = $_[1];
+    return unless defined $key;
+    if ($key =~ /^([-\w]+)==(.+)$/) {
+        return ($2 eq $conf->data->get($1));
+    }
+    else {
+        return exists($conf->data->{c}->{$key})
+            ? ($conf->data()->get($key) ? 1 : 0)
+            : $key eq $conf->data()->get('osname');
+    }
+}
+
+sub truth { $_[0] ? "true" : "false"; }
+
+# Recursively evaluate boolean expressions with multiple keys and | & ! ops.
+# Order of precedence: Just "!" and "NOT" binds tighter than AND and OR.
+# There's no precedence for AND over OR defined, just left to right.
+sub cond_eval {
+    my $conf = $_[0];
+    my $expr = $_[1];
+    my @count = split /[\s!&|\(]+/, $expr; # optimizable with tr
+    if (@count > 1) { # multiple keys: recurse into
+        my $truth = 0;
+        my $prevtruth = 0;
+        my $key = next_expr($expr);
+        my $op  = '';
+      LOOP:
+        while ($key) {
+            if (($key eq '!') or (uc($key) eq 'NOT')) {
+                # bind next key immediately
+                $op = 'NOT';
+                $key = next_expr($expr);
+            }
+            elsif ($truth and ($op eq 'OR')) {
+                # true OR: => true
+                last LOOP;
+            }
+            $prevtruth = $truth;
+            if (!$truth and ($op eq 'AND')) { # false AND: => false, skip rest
+                last LOOP;
+            }
+            $truth = cond_eval($conf, $key);
+            if ($op eq 'NOT') { # NOT *: invert
+                $truth = $truth ? 0 : 1;
+            }
+            elsif ($op eq 'AND' and !$truth) { # * AND false: => false
+                last LOOP;
+            }
+            # * OR false => * (keep $truth). true OR * already handled before
+            my $prevexpr = $expr;
+            $op  = next_expr($expr);
+            if ($op) {
+                if ($op eq '|' or uc($op) eq 'OR') {
+                    $op = 'OR';
+                }
+                elsif ($op eq '&' or uc($op) eq 'AND') {
+                    $op = 'AND';
+                }
+                elsif ($op eq '!' or uc($op) eq 'NOT') {
+                    $op = 'NOT';
+                }
+                else {
+                    die "invalid op \"$op\" in \"$_[1]\" at \"$prevexpr\".\n";
+                }
+                $key = next_expr($expr);
+            }
+            elsif ($prevexpr) {
+                die "Makefile conditional syntax error: missing op in 
\"$_[1]\" at \"$prevexpr\".\n";
+            }
+            else {
+                last LOOP; # end of expr, nothing left
+            }
+            if ($prevexpr eq $expr) {
+                die "Makefile conditional parser error in \"$_[1]\" at 
\"$prevexpr\".\n";
+            }
+        }
+        return $truth;
+    }
+    cond_eval_single($conf, $expr);
+}
+
 sub append_configure_log {
     my $conf = shift;
     my $target = shift;

Modified: trunk/t/steps/gen_makefiles-01.t
==============================================================================
--- trunk/t/steps/gen_makefiles-01.t    (original)
+++ trunk/t/steps/gen_makefiles-01.t    Thu Jan  1 12:16:59 2009
@@ -5,9 +5,80 @@
 
 use strict;
 use warnings;
-use Test::More tests =>  7;
+my @cond_tests;
+my @conf_args = ( true => 1, false => 0, value => 'xx' );
+BEGIN {
+    @cond_tests =
+      (
+       # perl-syntax       true or false
+       ["IF(true)",            1],
+       ["IF(false)",           0],
+       ["UNLESS(true)",                0],
+       ["UNLESS(false)",       1],
+       ["IF(true | false)",    1],
+       ["IF(true & false)",     0],
+       ["IF(true or true)",     1],
+       ["IF(true or false)",    1],
+       ["IF(false or true)",    1],
+       ["IF(false or false)",   0],
+       ["IF(true and true)",    1],
+       ["IF(true and false)",   0],
+       ["IF(false and true)",   0],
+       ["IF(false and false)",  0],
+       ["UNLESS(true|false)",   0],
+       ["UNLESS(true&false)",   1],
+       ["IF(!false)",          1],
+       ["IF(true)",            1],
+       ["ELSIF(value)",        0],
+       ["ELSE",                0],
+       ["IF(false)",           0],
+       ["ELSIF(value)",        1],
+       ["ELSE",                0],
+       ["IF(false)",           0],
+       ["ELSIF(false)",        0],
+       ["ELSE",                1],
+       # Exercise the parser
+       ["IF(true and (!false and value))",  1],
+       ["IF(true and (!false) and value)",  1],
+       ["IF(true and !false and value)",    1, 'no parens'],
+       ["IF(true and not false and value)", 1, 'no parens'],
+       ["IF(true&!false&value)",            1],
+       ["IF(false or (!false and value))",  1, 'not parser problem'],
+       ["UNLESS(!(true&!false&value))",     1, 'no ws, but nested parens'],
+       ["IF(true&(!false&false))",          0, 'not precedence'],
+       ["IF(true&(!false&value))",          1],
+       ["IF(not true and value)",           0, 'not precedence over and'],
+       ["IF(not false and value)",          1],
+       ["IF((not false) and value)",        1],
+       ["IF(not (false and value))",        1],
+       ["IF(not (false or value))",         0],
+       ["IF(true and not false)",           1],
+       # platform
+       ["IF(someplatform)",                1],
+       ["IF(not someplatform)",                    0],
+       ["UNLESS(someplatform)",                    0],
+       ["UNLESS(not someplatform)",        1],
+       # key==value
+       ["IF(value==xx)",                    1],
+       ["IF(value==xxy)",                   0],
+       ["UNLESS(value==xx)",                0],
+       ["UNLESS(value==xxy)",               1],
+       ["IF(true & (value==xx & (!false)))",1],
+       # These are invalid:
+       #["IF(value == xx)",                  0], # invalid op error
+       #["IF(value = xx)",                   0], # invalid op error
+       ["IF(value=xx)",                     0], # also invalid, no warning. 
checks for key value=xx
+
+       # Legacy syntax                 true or false
+       ["CONDITIONED_LINE(true)",          1],
+       ["INVERSE_CONDITIONED_LINE(true)",   0],
+       ["CONDITIONED_LINE(false)",         0],
+       ["INVERSE_CONDITIONED_LINE(false)",  1],
+      );
+}
+use Test::More tests => (7 + scalar(@cond_tests));
 use Carp;
-use lib qw( lib );
+use lib qw( . lib );
 use_ok('config::gen::makefiles');
 use Parrot::Configure;
 use Parrot::Configure::Options qw( process_options );
@@ -38,6 +109,52 @@
 is($missing_SOURCE, 0, "No Makefile source file missing");
 ok(-f $step->{CFLAGS_source}, "CFLAGS source file located");
 
+my $index = undef;
+sub result {
+    my $c = shift;
+    my $s = $c->[0];
+    $s =~ s/^\+/plus_/;
+    $s =~ s/^\-/minus_/;
+    $s =~ s/\|/OR/g;
+    $s =~ s/\&/AND/g;
+    $s =~ s/\!/NOT/g;
+    $s =~ s/[\()]//g;
+    $s =~ s/ /_/g;
+    $s .= ("_".++$index) if $s =~ /^(ELSE|ELSIF)/;
+    return $s."=".($c->[1]?"true":"false");
+}
+# test #IF(keys):line
+$conf->data->set( @conf_args, ('osname' => 'someplatform' ) );
+open IN, ">", "Makefile_$$.in";
+print IN "# There should only be =true results in .out\n";
+for my $c (@cond_tests) {
+    my $result = result($c);
+    print IN "#$c->[0]:$result\n";
+}
+close IN;
+$conf->genfile("Makefile_$$.in", "Makefile_$$.out",
+              (makefile => 1, conditioned_lines => 1));
+open OUT, "<", "Makefile_$$.out";
+my $f;
+{
+    local $/;
+    $f = <OUT>;
+}
+END {
+    unlink "Makefile_$$.in", "Makefile_$$.out";
+}
+$index = undef;
+for my $c (@cond_tests) {
+    my $result = result($c);
+    if ($c->[2] and $c->[2] =~ /^TODO(.*)$/) {
+        local $TODO = $1;
+        ok(($c->[1] ? $f =~ /^$result$/m : $f !~ /^$result$/m), "$result");
+    }
+    else {
+        ok(($c->[1] ? $f =~ /^$result$/m : $f !~ /^$result$/m), 
"$result".($c->[2]?" $c->[2]":''));
+    }
+}
+
 pass("Completed all tests in $0");
 
 ################### DOCUMENTATION ###################

Reply via email to