列表和一元运算符组合的 Perl 运算符优先级

Perl operator precendece for a combination of list and unary operators

我遇到了一个奇怪的案例,我想它与运算符优先级有关。考虑这个测试程序:

use strict;
use warnings;
use Test::More;

my $fn = 'dummy';
ok( ! -e $fn, 'file does not exists' );
ok( not -e $fn, 'file does not exists' );
done_testing();

输出为:

ok 1 - file does not exists
not ok 2
#   Failed test at ./p.pl line 10.
1..2
# Looks like you failed 1 test of 2.

问题是:为什么第二次测试失败了? ($fn 假定已知不存在)

另请参阅:List Operator Precedence in Perl.


阅读perlop后,我的猜测是这里至少涉及五个操作员:

perl -MO=Deparse 表明您的代码被解释为:

use Test::More;
use warnings;
use strict;
my $fn = 'dummy';
ok !(-e $fn), 'file does not exists';
ok !(-e $fn, 'file does not exists');
done_testing();

-e $fn 为假。

'file does not exists' 本质上是正确的。

因此,列表 (-e $fn, 'file does not exists') 是正确的。

因此,!(...)为假,测试失败。

与perlop直接相关,你需要注意的是!,之上,在table:

之上
       right       ! ~ \ and unary + and -
...
       left        , =>
...
       right       not

也就是说,逗号将事物组合在一起 "more tightly" 比 not 做的多,但 "less tightly" 比 ! 做的多。

重新阅读 perlop 文档后,我认为这是正在发生的事情:

ok( not -e $fn, 'file does not exists' );

Perl 从左到右解析这条语句。它遇到的第一件事是 函数调用 (也称为 列表运算符 ,如果该函数是内置的或使用原型并在列表上运行)。函数调用 ok( ... )。在文档中被描述为 TERM

A TERM has the highest precedence in Perl. They include variables, quote and quote-like operators, any expression in parentheses, and any function whose arguments are parenthesized.

A list operator(在perlop页面中没有准确定义,但在perlsub页面中简要提及)是如果后跟括号,也被视为 TERMperlop 表示:

If any list operator (print(), etc.) or any unary operator (chdir(), etc.) is followed by a left parenthesis as the next token, the operator and arguments within parentheses are taken to be of highest precedence, just like a normal function call.

现在解析器继续使用表达式 not -e $fn, 'file does not exists'。也就是说,它必须解析 ok 函数的参数。 它在这里首先遇到的是 not 运算符。文档说:

Unary "not" returns the logical negation of the expression to its right. It's the equivalent of "!" except for the very low precedence.

那么它必须确定"the expression to its right"。在这里,解析器找到文件测试运算符 -e。文档说:

Regarding precedence, the filetest operators, like -f , -M , etc. are treated like named unary operators, but they don't follow this functional parenthesis rule. That means, for example, that -f($file).".bak" is equivalent to -f "$file.bak" .

The various named unary operators are treated as functions with one argument, with optional parentheses.

现在 一元运算符 (没有后面的括号)比 not 运算符具有更高的优先级,因此解析器继续,尝试确定 -e 运算符。它现在遇到一个新的 TERM,(我们现在正在考虑这个表达式:$fn, 'file does not exists')。 TERM$fn 并且由于 TERM 具有最高优先级,因此会立即对其进行评估。然后它继续到逗号运算符。由于逗号运算符的优先级低于 filetest 运算符,并且 filetest 运算符是一元的(仅采用单个参数),因此解析器决定它已完成 filetest 运算符的参数并评估 -e $fn。然后它继续逗号:

Binary "," is the comma operator. In scalar context it evaluates its left argument, throws that value away, then evaluates its right argument and returns that value. This is just like C's comma operator. In list context, it's just the list argument separator, and inserts both its arguments into the list. These arguments are also evaluated from left to right.

由于逗号运算符的优先级高于 not 运算符,因此解析器发现它仍未完成 not 的参数。相反,它发现 not 的参数是一个列表(由于逗号运算符),它已经评估了逗号运算符的左侧参数 -e $fn,并丢弃该值,并继续逗号运算符的右参数是字符串 'file does not exists'。这被评估,然后解析器找到右括号 ),这意味着 not 的参数是后面的字符串。否定非空字符串是错误的。

最后,解析器发现 ok 函数的参数为​​假,并运行 ok( 0 ).

Why does the second test fail?

因为 Perl 的解析器处理 !not 的方式不同。您可以在 Perl 的语法中看到这一点,该语法在 Perl 源代码的 perly.y 中定义。

! 的规则在解析器遇到 ! 后跟一个术语时立即生效:

    |       '!' term                               /* !$x */
                    { $$ = newUNOP(OP_NOT, 0, scalar()); }

另一方面,not 的规则仅在解析器遇到 not 后跟列表表达式(由逗号 * 连接的术语列表)时才会生效:

    |       NOTOP listexpr                       /* not $foo */
                    { $$ = newUNOP(OP_NOT, 0, scalar()); }

请注意,这两个规则的操作是相同的:将类型为 OP_NOT 的新一元操作码添加到解析树。操作数是标量上下文中的第二个参数(termlistexpr)。


* 或单个术语,但这具有非常低的优先级。

跟踪解析

您可以通过使用 -DDEBUGGING 和 运行 -Dpv 编译 perl 来查看上述规则的作用,这会打开 debug flags 进行标记化和解析。

这是解析器对 ! 所做的:

$ perl -Dpv -e'ok(! -e "foo", "bar")'
...

Next token is token '(' (0x1966e98)
Shifting token '(', Entering state 185
Reading a token:
Next token is token '!' (0x1966e98)
Shifting token '!', Entering state 49
Reading a token:
Next token is token UNIOP (0x110)
Shifting token UNIOP, Entering state 39
Reading a token:
Next token is token THING (0x1966e58)
Shifting token THING, Entering state 25

index:        2        3        4        5        6        7        8        9
state:        8       15      103       68      185       49       39       25
token:       @1 remember  stmtseq    amper      '('      '!'    UNIOP    THING
value:        0       22 (Nullop)    rv2cv 26635928 26635928      272    const

Reducing stack by rule 184 (line 961), THING -> term
Entering state 128
Reading a token:
Next token is token ',' (0x1966e58)

index:        2        3        4        5        6        7        8        9
state:        8       15      103       68      185       49       39      128
token:       @1 remember  stmtseq    amper      '('      '!'    UNIOP     term
value:        0       22 (Nullop)    rv2cv 26635928 26635928      272    const

Reducing stack by rule 199 (line 999), UNIOP term -> term
Entering state 150
Next token is token ',' (0x1966e58)

index:        1        2        3        4        5        6        7        8
state:        1        8       15      103       68      185       49      150
token: GRAMPROG       @1 remember  stmtseq    amper      '('      '!'     term
value:        0        0       22 (Nullop)    rv2cv 26635928 26635928     ftis

Reducing stack by rule 148 (line 829), '!' term -> termunop
Entering state 62

index:        1        2        3        4        5        6        7
state:        1        8       15      103       68      185       62
token: GRAMPROG       @1 remember  stmtseq    amper      '(' termunop
value:        0        0       22 (Nullop)    rv2cv 26635928      not

...

换句话说,解析器读入

( ! -e "foo"

-e "foo" 减少为 term,然后将逻辑否定操作码添加到解析树。在标量上下文中,操作数是 -e "foo"


这是解析器对 not 所做的:

$ perl -Dpv -e'ok(not -e "foo", "bar")'
...

Reading a token:
Next token is token '(' (0x26afed8)
Shifting token '(', Entering state 185
Reading a token:
Next token is token NOTOP (0x26afed8)
Shifting token NOTOP, Entering state 48
Reading a token:
Next token is token UNIOP (0x110)
Shifting token UNIOP, Entering state 39
Reading a token:
Next token is token THING (0x26afe98)
Shifting token THING, Entering state 25

index:        2        3        4        5        6        7        8        9
state:        8       15      103       68      185       48       39       25
token:       @1 remember  stmtseq    amper      '('    NOTOP    UNIOP    THING
value:        0       22 (Nullop)    rv2cv 40566488 40566488      272    const

Reducing stack by rule 184 (line 961), THING -> term
Entering state 128
Reading a token:
Next token is token ',' (0x26afe98)

index:        2        3        4        5        6        7        8        9
state:        8       15      103       68      185       48       39      128
token:       @1 remember  stmtseq    amper      '('    NOTOP    UNIOP     term
value:        0       22 (Nullop)    rv2cv 40566488 40566488      272    const

Reducing stack by rule 199 (line 999), UNIOP term -> term
Entering state 65
Next token is token ',' (0x26afe98)

index:        1        2        3        4        5        6        7        8
state:        1        8       15      103       68      185       48       65
token: GRAMPROG       @1 remember  stmtseq    amper      '('    NOTOP     term
value:        0        0       22 (Nullop)    rv2cv 40566488 40566488     ftis

Reducing stack by rule 105 (line 683), term -> listexpr
Entering state 149
Next token is token ',' (0x26afe98)
Shifting token ',', Entering state 162
Reading a token:
Next token is token THING (0x26afdd8)
Shifting token THING, Entering state 25

index:        3        4        5        6        7        8        9       10
state:       15      103       68      185       48      149      162       25
token: remember  stmtseq    amper      '('    NOTOP listexpr      ','    THING
value:       22 (Nullop)    rv2cv 40566488 40566488     ftis 40566424    const

Reducing stack by rule 184 (line 961), THING -> term
Entering state 249
Reading a token:
Next token is token ')' (0x26afdd8)

index:        3        4        5        6        7        8        9       10
state:       15      103       68      185       48      149      162      249
token: remember  stmtseq    amper      '('    NOTOP listexpr      ','     term
value:       22 (Nullop)    rv2cv 40566488 40566488     ftis 40566424    const

Reducing stack by rule 104 (line 678), listexpr ',' term -> listexpr
Entering state 149
Next token is token ')' (0x26afdd8)

index:        1        2        3        4        5        6        7        8
state:        1        8       15      103       68      185       48      149
token: GRAMPROG       @1 remember  stmtseq    amper      '('    NOTOP listexpr
value:        0        0       22 (Nullop)    rv2cv 40566488 40566488     list

Reducing stack by rule 196 (line 993), NOTOP listexpr -> term
Entering state 65
Next token is token ')' (0x26afdd8)

index:        1        2        3        4        5        6        7
state:        1        8       15      103       68      185       65
token: GRAMPROG       @1 remember  stmtseq    amper      '('     term
value:        0        0       22 (Nullop)    rv2cv 40566488      not

...

换句话说,解析器读入

( not -e "foo"

-e "foo" 减少为 term,读入

, "bar"

term, "bar" 减少为 listexpr,然后将逻辑否定操作码添加到解析树。在标量上下文中,操作数是 -e "foo", "bar"


因此,尽管两个逻辑非的操作码相同,但它们的操作数不同。您可以通过检查生成的解析树看到这一点:

$ perl -MO=Concise,-tree -e'ok(! -e "foo", "bar")'
<a>leave[1 ref]-+-<1>enter
                |-<2>nextstate(main 1 -e:1)
                `-<9>entersub[t1]---ex-list-+-<3>pushmark
                                            |-<6>not---<5>ftis---<4>const(PV "foo")
                                            |-<7>const(PV "bar")
                                            `-ex-rv2cv---<8>gv(*ok)
-e syntax OK
$ perl -MO=Concise,-tree -e'ok(not -e "foo", "bar")'
<c>leave[1 ref]-+-<1>enter
                |-<2>nextstate(main 1 -e:1)
                `-<b>entersub[t1]---ex-list-+-<3>pushmark
                                            |-<9>not---<8>list-+-<4>pushmark
                                            |                  |-<6>ftis---<5>const(PV "foo")
                                            |                  `-<7>const(PV "bar")
                                            `-ex-rv2cv---<a>gv(*ok)
-e syntax OK

对于!,否定作用于文件test:

|-<6>not---<5>ftis

虽然使用 not,但否定作用于列表:

|-<9>not---<8>list

您还可以使用 B::Deparse 将解析树转储为 Perl 代码,它以不同的格式显示相同的内容:

$ perl -MO=Deparse,-p -e'ok(! -e "foo", "bar")'
ok((!(-e 'foo')), 'bar');
-e syntax OK
$ perl -MO=Deparse,-p -e'ok(not -e "foo", "bar")'
ok((!((-e 'foo'), 'bar')));
-e syntax OK

对于!,否定作用于文件test:

!(-e 'foo')

虽然使用 not,但否定作用于列表:

!((-e 'foo'), 'bar')

并且作为 ,标量上下文中的列表计算为列表中的最后一项,给出

ok( ! 'bar' );

其中 ! 'bar' 是假的。