develooper Front page | perl.perl5.porters | Postings from February 2008

optimisations you just can't do (was Re: Interesting self contained task)

From:
Nicholas Clark
Date:
February 25, 2008 03:59
Subject:
optimisations you just can't do (was Re: Interesting self contained task)
Message ID:
20080225115921.GW38653@plum.flirble.org
On Sat, Feb 23, 2008 at 08:04:13AM +0000, Nicholas Clark wrote:

> Which, of course, is an alternative "interesting self contained task"
> 
> As is, I think, optimising constant pack unless the pattern contains 'p' or 'P'

My original two takers for the original task have now deresed, it seems.

> and (possibly) optimising unpack, but only in scalar context. (Not sure how to
> do that one)

Um, so after quite a bit of investigation, I think that the answer is that
"you can't". At least, really not easily.

[The following is all based on my limited understanding of how the optree
works, and investigating how it behaves currently. Corrections welcome.]

Specifically, you can only constant fold scalar constants.

Fine, so add a case in Perl_fold_constants() in

     switch (type) {
	...

something like:
	case OP_UNPACK:
	    if ((PL_op->op_flags & OPf_WANT) != OPf_WANT_SCALAR)
		goto nope:
	break;

Except that that doesn't do anything, because at the time that constant
folding happens, ops don't yet know their context. *That* happens in
Perl_scalar(). OK, so add it there. The return type is already correct.
Only we hit a problem. Here's a typical use case:

OP *
Perl_scalarkids(pTHX_ OP *o)
{
    if (o && o->op_flags & OPf_KIDS) {
        OP *kid;
	for (kid = cLISTOPo->op_first; kid; kid = kid->op_sibling)
	    scalar(kid);
    }
    return o;
}


void context. It is assumed that Perl_scalar() doesn't change the op pointed
to.

OK. We can fix that?

So I tried. And it all becomes rather complex. In fact, I couldn't make it
work for actually folding scalars, so I tried an identity operation in
Perl_scalar - if it's an unpack op, just copy it, free the old one, and return
the new address. (Note for the naïve - ops free recursively, so if you don't
want an explosion of double free errors, you need "orphan" the children:

	o->op_flags &= ~OPf_KIDS;
)


And still there were errors. Perl_ck_fun was particular fun, because it has
to fix up existing parent ops. And still there were errors.

Specifically we get down to two.

Perl_ck_split failed an assertion, which revealed somewhere else that needed
fixing (update op->op_last if you change op->op_first->op_sibling). But the
failing test case is the last in pp_pack.t:

{
    #50256
    my ($v) = split //, unpack ('(B)*', 'ab');
    is($v, 0); # Doesn't SEGV :-)
}


So, danger, I would not even have found that problem 3 weeks ago. Clearly there
is a risk of other use cases for unpack not being tested. So trying to change
the rules for the general case (allowing Perl_scalar() to change the op (tree)
and thereby permit optimisations) is likely to be more fraught still.

Fixed that. And there there was one. This part of t/op/pack.t fails:

    my $got = $what eq 'u' ? (unpack $template, $in) : (pack $template, $in);

$ ./perl t/op/pack.t | grep ^not
# Failed at t/op/pack.t line 515
#      got undef
# expected 'foobar'
# Failed at t/op/pack.t line 515
#      got undef
# expected 'foobar'
# Failed at t/op/pack.t line 515
#      got undef
# expected 'foobar '
# Failed at t/op/pack.t line 515
#      got undef
# expected 'foobar '
# Failed at t/op/pack.t line 515
#      got undef
# expected 'foo'
# Failed at t/op/pack.t line 515
#      got undef
# expected 'foo'
not ok 450
not ok 451
not ok 454
not ok 455
not ok 459
not ok 460

There is no opportunity to optimise *there*, but there might be for constant
unpack arguments. Whatever, it should not fail.

The optree should look like this:

$ perl -MO=Concise -le 'my $got = $what eq "u" ? (unpack $template, $in) : (pack $template, $in);'
c  <@> leave[1 ref] vKP/REFC ->(end)
1     <0> enter ->2
2     <;> nextstate(main 1 -e:1) v ->3
b     <2> sassign vKS/2 ->c
-        <1> null sK/1 ->a
6           <|> cond_expr(other->7) sK/1 ->d
5              <2> seq sK/2 ->6
-                 <1> ex-rv2sv sK/1 ->4
3                    <#> gvsv[*what] s ->4
4                 <$> const[PV "u"] s ->5
9              <@> unpack sKP/2 ->a
-                 <0> ex-pushmark s ->7
-                 <1> ex-rv2sv sK/1 ->8
7                    <#> gvsv[*template] s ->8
-                 <1> ex-rv2sv sK/1 ->9
8                    <#> gvsv[*in] s ->9
g              <@> pack[t7] sKP/2 ->a
d                 <0> pushmark s ->e
-                 <1> ex-rv2sv sK/1 ->f
e                    <#> gvsv[*template] s ->f
-                 <1> ex-rv2sv sK/1 ->g
f                    <#> gvsv[*in] s ->g
a        <0> padsv[$got:1,2] sRM*/LVINTRO ->b
-e syntax OK

The best I can get it to is something like this:

$ ./perl -Ilib -MO=Concise -le 'my $got = $what eq "u" ? (unpack $template, $in) : (pack $template, $in);'
a  <@> leave[1 ref] vKP/REFC ->(end)
1     <0> enter ->2
2     <;> nextstate(main 1 -e:1) v:{ ->3
9     <2> sassign vKS/2 ->a
-        <1> null sK/1 ->f
6           <|> cond_expr(other->7) sK/1 ->b
5              <2> seq sK/2 ->6
-                 <1> ex-rv2sv sK/1 ->4
3                    <#> gvsv[*what] s ->4
4                 <$> const[PV "u"] s ->5
-              <@> unpack sKP/2 ->-
-                 <0> ex-pushmark s ->7
-                 <1> ex-rv2sv sK/1 ->8
7                    <#> gvsv[*template] s ->8
-                 <1> ex-rv2sv sK/1 ->9
8                    <#> gvsv[*in] s ->9
e              <@> pack[t7] sKP/2 ->f
b                 <0> pushmark s ->c
-                 <1> ex-rv2sv sK/1 ->d
c                    <#> gvsv[*template] s ->d
-                 <1> ex-rv2sv sK/1 ->e
d                    <#> gvsv[*in] s ->e
f        <0> padsv[$got:1,2] sRM*/LVINTRO ->9
-e syntax OK

Problem is that that unpack op isn't linked in properly. The above is an
attempt to force re-linking by clearing op_next, but that doesn't work.
As doesn't calling LINKLIST() inside Perl_scalar.

In turn, this just isn't going to work, because (I infer) the re-linking has
to happen somewhere from a parent op, and there is no general way to work out
which op is the parent. As far as I can work out, linking is assumed to happen
exactly once, and isn't expected to be re-done. In this case linking has
already happened in Perl_newASSIGNOP() to work out whether there are common
variables, before Perl_scalar() gets called.

Plus, even if one *could* get all this relinking to work in the general case,
even for all ops, it's *still* not going to achieve much in the general case,
because:

    constant folding works as ops are built.

Specifically, constant folding is done one op at a time, from the inside out,
and just once. So for each op, only if the children are in this set:

    for (curop = LINKLIST(o); curop != o; curop = LINKLIST(curop)) {
	const OPCODE type = curop->op_type;
	if ((type != OP_CONST || (curop->op_private & OPpCONST_BARE)) &&
	    type != OP_LIST &&
	    type != OP_SCALAR &&
	    type != OP_NULL &&
	    type != OP_PUSHMARK)
	{
	    goto nope;
	}
    }

will it be even considered.

So attempting to do a late optimisation of unpack (or anything else) in
Perl_scalar(), won't cause the resulting constant to be propagated outwards
into any surrounding constant expression.

I guess the "right" time to do this (if at all) would be another
post-everything optimisation phase, which either:

a: Knows that it can't use the op_next pointers, and forcibly re-links once
   at the end if anything changed.
b: Pessimistically re-links each time any individual optimisation triggers.

However, look on the upside, I did find 2 trip hazards, that I fixed:

http://public.activestate.com/cgi-bin/perlbrowse?patch_num=33368&show_patch=Show+Patch

Full (failed) patch appended for the record.

Nicholas Clark

==== //depot/perl/embed.fnc#589 - /Volumes/Stuff/p4perl/perl/embed.fnc ====
--- /tmp/tmp.70887.80	2008-02-25 11:51:58.000000000 +0100
+++ /Volumes/Stuff/p4perl/perl/embed.fnc	2008-02-23 11:11:01.000000000 +0100
@@ -796,7 +796,7 @@ Ap	|void	|save_padsv_and_mortalize|PADOF
 Ap	|void	|save_sptr	|NN SV** sptr
 Ap	|SV*	|save_svref	|NN SV** sptr
 p	|OP*	|sawparens	|NULLOK OP* o
-p	|OP*	|scalar		|NULLOK OP* o
+Rp	|OP*	|scalar		|NULLOK OP* o
 p	|OP*	|scalarkids	|NULLOK OP* o
 p	|OP*	|scalarseq	|NULLOK OP* o
 p	|OP*	|scalarvoid	|NN OP* o
==== //depot/perl/op.c#992 - /Volumes/Stuff/p4perl/perl/op.c ====
--- /tmp/tmp.70887.128	2008-02-25 11:51:59.000000000 +0100
+++ /Volumes/Stuff/p4perl/perl/op.c	2008-02-25 11:51:15.000000000 +0100
@@ -816,9 +816,11 @@ OP *
 Perl_scalarkids(pTHX_ OP *o)
 {
     if (o && o->op_flags & OPf_KIDS) {
-        OP *kid;
-	for (kid = cLISTOPo->op_first; kid; kid = kid->op_sibling)
-	    scalar(kid);
+	OP **kid_p = &(cLISTOPo->op_first);
+	while (*kid_p) {
+	    *kid_p = scalar(*kid_p);
+	    kid_p = &((*kid_p)->op_sibling);
+	}
     }
     return o;
 }
@@ -861,13 +863,16 @@ Perl_scalar(pTHX_ OP *o)
 
     switch (o->op_type) {
     case OP_REPEAT:
-	scalar(cBINOPo->op_first);
+	cBINOPo->op_first = scalar(cBINOPo->op_first);
 	break;
     case OP_OR:
     case OP_AND:
     case OP_COND_EXPR:
-	for (kid = cUNOPo->op_first->op_sibling; kid; kid = kid->op_sibling)
-	    scalar(kid);
+	kid = cUNOPo->op_first;
+	while (kid && kid->op_sibling) {
+	    kid->op_sibling = scalar(kid->op_sibling);
+	    kid = kid->op_sibling;
+	}
 	break;
     case OP_SPLIT:
 	if ((kid = cLISTOPo->op_first) && kid->op_type == OP_PUSHRE) {
@@ -881,36 +886,63 @@ Perl_scalar(pTHX_ OP *o)
     case OP_NULL:
     default:
 	if (o->op_flags & OPf_KIDS) {
-	    for (kid = cUNOPo->op_first; kid; kid = kid->op_sibling)
-		scalar(kid);
+	    OP **kid_p = &(cUNOPo->op_first);
+	    while (*kid_p) {
+		*kid_p = scalar(*kid_p);
+		kid_p = &((*kid_p)->op_sibling);
+	    }
 	}
 	break;
     case OP_LEAVE:
     case OP_LEAVETRY:
-	kid = cLISTOPo->op_first;
-	scalar(kid);
-	while ((kid = kid->op_sibling)) {
-	    if (kid->op_sibling)
-		scalarvoid(kid);
+	kid = cLISTOPo->op_first = scalar(cLISTOPo->op_first);
+	while (kid && kid->op_sibling) {
+	    if (kid->op_sibling->op_sibling)
+		scalarvoid(kid->op_sibling);
 	    else
-		scalar(kid);
+		kid->op_sibling = scalar(kid->op_sibling);
+	    kid = kid->op_sibling;
 	}
 	PL_curcop = &PL_compiling;
 	break;
     case OP_SCOPE:
     case OP_LINESEQ:
     case OP_LIST:
-	for (kid = cLISTOPo->op_first; kid; kid = kid->op_sibling) {
-	    if (kid->op_sibling)
-		scalarvoid(kid);
-	    else
-		scalar(kid);
+	{
+	    OP **kid_p = &(cLISTOPo->op_first);
+	    while (*kid_p) {
+		if ((*kid_p)->op_sibling)
+		    scalarvoid(*kid_p);
+		else
+		    *kid_p = scalar(*kid_p);
+		kid_p = &((*kid_p)->op_sibling);
+	    }
 	}
 	PL_curcop = &PL_compiling;
 	break;
     case OP_SORT:
 	if (ckWARN(WARN_VOID))
 	    Perl_warner(aTHX_ packWARN(WARN_VOID), "Useless use of sort in scalar context");
+	break;
+    case OP_UNPACK:
+	{
+	    LISTOP *newo;
+	    NewOp(1101, newo, 1, LISTOP);
+	    /* PerlIO_printf(PerlIO_stderr(), "%p %p\n", o, newo); */
+	    *newo = *(LISTOP*)o;
+	    o->op_flags &= ~OPf_KIDS;
+	    if (newo->op_next) {
+		newo->op_next = NULL;
+		/* KHAAAN. This just doesn't work, as this isn't the right place
+		   to start. Changing ops around in the middle of a linked tree
+		   is just too complex.  */
+		LINKLIST(newo);
+	    }
+	    op_free(o);
+	    o = (OP*)newo;
+	}
+	break;
+
     }
     return o;
 }
@@ -2432,7 +2464,7 @@ Perl_fold_constants(pTHX_ register OP *o
     PERL_ARGS_ASSERT_FOLD_CONSTANTS;
 
     if (PL_opargs[type] & OA_RETSCALAR)
-	scalar(o);
+	o = scalar(o);
     if (PL_opargs[type] & OA_TARGET && !o->op_targ)
 	o->op_targ = pad_alloc(type, SVs_PADTMP);
 
@@ -3018,7 +3050,7 @@ Perl_newOP(pTHX_ I32 type, I32 flags)
     o->op_next = o;
     o->op_private = (U8)(0 | (flags >> 8));
     if (PL_opargs[type] & OA_RETSCALAR)
-	scalar(o);
+	o = scalar(o);
     if (PL_opargs[type] & OA_TARGET)
 	o->op_targ = pad_alloc(type, SVs_PADTMP);
     return CHECKOP(type, o);
@@ -3691,7 +3723,7 @@ Perl_newSVOP(pTHX_ I32 type, I32 flags, 
     svop->op_next = (OP*)svop;
     svop->op_flags = (U8)flags;
     if (PL_opargs[type] & OA_RETSCALAR)
-	scalar((OP*)svop);
+	svop = (SVOP *) scalar((OP*)svop);
     if (PL_opargs[type] & OA_TARGET)
 	svop->op_targ = pad_alloc(type, SVs_PADTMP);
     return CHECKOP(type, svop);
@@ -3717,7 +3749,7 @@ Perl_newPADOP(pTHX_ I32 type, I32 flags,
     padop->op_next = (OP*)padop;
     padop->op_flags = (U8)flags;
     if (PL_opargs[type] & OA_RETSCALAR)
-	scalar((OP*)padop);
+	padop = (PADOP *)scalar((OP*)padop);
     if (PL_opargs[type] & OA_TARGET)
 	padop->op_targ = pad_alloc(type, SVs_PADTMP);
     return CHECKOP(type, padop);
@@ -3751,7 +3783,7 @@ Perl_newPVOP(pTHX_ I32 type, I32 flags, 
     pvop->op_next = (OP*)pvop;
     pvop->op_flags = (U8)flags;
     if (PL_opargs[type] & OA_RETSCALAR)
-	scalar((OP*)pvop);
+	pvop = (PVOP*) scalar((OP*)pvop);
     if (PL_opargs[type] & OA_TARGET)
 	pvop->op_targ = pad_alloc(type, SVs_PADTMP);
     return CHECKOP(type, pvop);
@@ -4318,9 +4350,10 @@ Perl_newASSIGNOP(pTHX_ I32 flags, OP *le
 	return newBINOP(OP_NULL, flags, mod(scalar(left), OP_SASSIGN), scalar(right));
     }
     else {
+	right = scalar(right);
 	PL_eval_start = right;	/* Grandfathering $[ assignment here.  Bletch.*/
 	o = newBINOP(OP_SASSIGN, flags,
-	    scalar(right), mod(scalar(left), OP_SASSIGN) );
+	    right, mod(scalar(left), OP_SASSIGN) );
 	if (PL_eval_start)
 	    PL_eval_start = 0;
 	else {
@@ -4801,7 +4834,7 @@ whileline, OP *expr, OP *block, OP *cont
 
     if (expr) {
 	PL_parser->copline = (line_t)whileline;
-	scalar(listop);
+	listop = scalar(listop);
 	o = new_logop(OP_AND, 0, &expr, &listop);
 	if (o == expr && o->op_type == OP_CONST && !SvTRUE(cSVOPo->op_sv)) {
 	    op_free(expr);		/* oops, it's a while (0) */
@@ -6454,7 +6487,7 @@ Perl_ck_eval(pTHX_ OP *o)
 	    return o;
 	}
 	else {
-	    scalar((OP*)kid);
+	    cUNOPo->op_first = scalar((OP*)kid);
 	    PL_cv_has_eval = 1;
 	}
     }
@@ -6752,7 +6785,7 @@ Perl_ck_fun(pTHX_ OP *o)
 		{
 		    return too_many_arguments(o,PL_op_desc[type]);
 		}
-		scalar(kid);
+		*tokid = kid = scalar(kid);
 		break;
 	    case OA_LIST:
 		if (oa < 16) {
@@ -6939,7 +6972,7 @@ Perl_ck_fun(pTHX_ OP *o)
 		    kid->op_sibling = sibl;
 		    *tokid = kid;
 		}
-		scalar(kid);
+		*tokid = kid = scalar(kid);
 		break;
 	    case OA_SCALARREF:
 		mod(scalar(kid), type);
@@ -7066,11 +7099,11 @@ Perl_ck_grep(pTHX_ OP *o)
 	kid->op_next = (OP*)gwop;
 	o->op_flags &= ~OPf_STACKED;
     }
-    kid = cLISTOPo->op_first->op_sibling;
     if (type == OP_MAPWHILE)
-	list(kid);
+	kid = list(cLISTOPo->op_first->op_sibling);
     else
-	scalar(kid);
+	kid = cLISTOPo->op_first->op_sibling
+	    = scalar(cLISTOPo->op_first->op_sibling);
     o = ck_fun(o);
     if (PL_parser && PL_parser->error_count)
 	return o;
@@ -7453,7 +7486,7 @@ Perl_ck_repeat(pTHX_ OP *o)
 	cBINOPo->op_first = force_list(cBINOPo->op_first);
     }
     else
-	scalar(o);
+	o = scalar(o);
     return o;
 }
 
@@ -7789,7 +7822,15 @@ Perl_ck_split(pTHX_ OP *o)
 
     kid->op_type = OP_PUSHRE;
     kid->op_ppaddr = PL_ppaddr[OP_PUSHRE];
-    scalar(kid);
+
+    /* Arguably you can't simply retain the old value of kid, and check to see
+       if it has changed, as it will have become a pointer to free()d memory,
+       which in turn might just get allocated again. */
+    kid = scalar(kid);
+    if (cLISTOPo->op_first == cLISTOPo->op_last)
+	cLISTOPo->op_last = kid;
+    cLISTOPo->op_first = kid;
+
     if (((PMOP *)kid)->op_pmflags & PMf_GLOBAL && ckWARN(WARN_REGEXP)) {
       Perl_warner(aTHX_ packWARN(WARN_REGEXP),
                   "Use of /g modifier is meaningless in split");
@@ -7798,15 +7839,19 @@ Perl_ck_split(pTHX_ OP *o)
     if (!kid->op_sibling)
 	append_elem(OP_SPLIT, o, newDEFSVOP());
 
+    if (cLISTOPo->op_last == kid->op_sibling) {
+	cLISTOPo->op_last = kid->op_sibling = scalar(kid->op_sibling);
+    } else {
+	kid->op_sibling = scalar(kid->op_sibling);
+    }
     kid = kid->op_sibling;
-    scalar(kid);
 
     if (!kid->op_sibling)
 	append_elem(OP_SPLIT, o, newSVOP(OP_CONST, 0, newSViv(0)));
     assert(kid->op_sibling);
 
+    kid->op_sibling = scalar(kid->op_sibling);
     kid = kid->op_sibling;
-    scalar(kid);
 
     if (kid->op_sibling)
 	return too_many_arguments(o,OP_DESC(o));
@@ -7914,7 +7959,7 @@ Perl_ck_subr(pTHX_ OP *o)
 	    case '$':
 		proto++;
 		arg++;
-		scalar(o2);
+		prev->op_sibling = o2 = scalar(o2);
 		break;
 	    case '%':
 	    case '@':
@@ -7967,7 +8012,7 @@ Perl_ck_subr(pTHX_ OP *o)
 			}
 		    }
 		}
-		scalar(o2);
+		prev->op_sibling = o2 = scalar(o2);
 		break;
 	    case '[': case ']':
 		 goto oops;
@@ -8065,6 +8110,8 @@ Perl_ck_subr(pTHX_ OP *o)
 	}
 	else
 	    list(o2);
+	if (!o2)
+	    break;
 	mod(o2, OP_ENTERSUB);
 	prev = o2;
 	o2 = o2->op_sibling;
==== //depot/perl/perly.act#37 - /Volumes/Stuff/p4perl/perl/perly.act ====
--- /tmp/tmp.70887.175	2008-02-25 11:51:59.000000000 +0100
+++ /Volumes/Stuff/p4perl/perl/perly.act	2008-02-24 00:02:37.000000000 +0100
@@ -919,7 +919,7 @@ case 2:
   case 115:
 #line 877 "perly.y"
     {   if (IVAL((ps[(2) - (3)].val.i_tkval)) != OP_REPEAT)
-				scalar((ps[(1) - (3)].val.opval));
+				(ps[(1) - (3)].val.opval) = scalar((ps[(1) - (3)].val.opval));
 			    (yyval.opval) = newBINOP(IVAL((ps[(2) - (3)].val.i_tkval)), 0, (ps[(1) - (3)].val.opval), scalar((ps[(3) - (3)].val.opval)));
 			  TOKEN_GETMAD((ps[(2) - (3)].val.i_tkval),(yyval.opval),'o');
 			;}
==== //depot/perl/perly.h#37 - /Volumes/Stuff/p4perl/perl/perly.h ====
--- /tmp/tmp.70887.203	2008-02-25 11:51:59.000000000 +0100
+++ /Volumes/Stuff/p4perl/perl/perly.h	2008-02-24 00:02:37.000000000 +0100
@@ -203,7 +203,7 @@ typedef union YYSTYPE
     TOKEN* tkval;
 #endif
 }
-/* Line 1489 of yacc.c.  */
+/* Line 1529 of yacc.c.  */
 	YYSTYPE;
 # define yystype YYSTYPE /* obsolescent; will be withdrawn */
 # define YYSTYPE_IS_DECLARED 1
==== //depot/perl/perly.y#91 - /Volumes/Stuff/p4perl/perl/perly.y ====
--- /tmp/tmp.70887.206	2008-02-25 11:51:59.000000000 +0100
+++ /Volumes/Stuff/p4perl/perl/perly.y	2008-02-24 00:02:29.000000000 +0100
@@ -875,7 +875,7 @@ termbinop:	term ASSIGNOP term           
 			}
 	|	term MULOP term                        /* $x * $y, $x x $y */
 			{   if (IVAL($2) != OP_REPEAT)
-				scalar($1);
+				$1 = scalar($1);
 			    $$ = newBINOP(IVAL($2), 0, $1, scalar($3));
 			  TOKEN_GETMAD($2,$$,'o');
 			}
==== //depot/perl/pp_ctl.c#687 - /Volumes/Stuff/p4perl/perl/pp_ctl.c ====
--- /tmp/tmp.70887.264	2008-02-25 11:52:00.000000000 +0100
+++ /Volumes/Stuff/p4perl/perl/pp_ctl.c	2008-02-23 23:02:24.000000000 +0100
@@ -3063,13 +3063,13 @@ S_doeval(pTHX_ int gimme, OP** startop, 
 	    && cUNOPx(PL_eval_root)->op_first->op_type == OP_LINESEQ
 	    && cLISTOPx(cUNOPx(PL_eval_root)->op_first)->op_last->op_type
 	    == OP_REQUIRE)
-	scalar(PL_eval_root);
+	PL_eval_root = scalar(PL_eval_root);
     else if ((gimme & G_WANT) == G_VOID)
 	scalarvoid(PL_eval_root);
     else if ((gimme & G_WANT) == G_ARRAY)
 	list(PL_eval_root);
     else
-	scalar(PL_eval_root);
+	PL_eval_root = scalar(PL_eval_root);
 
     DEBUG_x(dump_eval());
 



nntp.perl.org: Perl Programming lists via nntp and http.
Comments to Ask Bjørn Hansen at ask@perl.org | Group listing | About