列表和一元运算符组合的 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页面中简要提及)是如果后跟括号,也被视为 TERM
。 perlop 表示:
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
的新一元操作码添加到解析树。操作数是标量上下文中的第二个参数(term
或 listexpr
)。
* 或单个术语,但这具有非常低的优先级。
跟踪解析
您可以通过使用 -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'
是假的。
我遇到了一个奇怪的案例,我想它与运算符优先级有关。考虑这个测试程序:
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页面中简要提及)是如果后跟括号,也被视为 TERM
。 perlop 表示:
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
的新一元操作码添加到解析树。操作数是标量上下文中的第二个参数(term
或 listexpr
)。
* 或单个术语,但这具有非常低的优先级。
跟踪解析
您可以通过使用 -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'
是假的。