为什么语句像1+;在Ruby中允许n*=3?

为什么语句像1+;在Ruby中允许n*=3?,ruby,Ruby,许多Ruby文档中的优先级表都列出了二进制算术运算,它们的优先级高于相应的复合赋值运算符。这让我相信像这样的代码不应该是有效的Ruby代码,但事实确实如此 1 + age *= 2 如果优先规则是正确的,我希望上面的代码会像下面这样插入括号: ((1 + age) *= 2) #ERROR: Doesn't compile 但事实并非如此 那么给出了什么呢?NB这个答案不应该被标记为解决了问题。有关正确的解释,请参见@Amadan的答案。 我不确定你提到的“许多Ruby文档”是什么,这里是最

许多Ruby文档中的优先级表都列出了二进制算术运算,它们的优先级高于相应的复合赋值运算符。这让我相信像这样的代码不应该是有效的Ruby代码,但事实确实如此

1 + age *= 2
如果优先规则是正确的,我希望上面的代码会像下面这样插入括号:

((1 + age) *= 2) #ERROR: Doesn't compile
但事实并非如此


那么给出了什么呢?

NB这个答案不应该被标记为解决了问题。有关正确的解释,请参见@Amadan的答案。


我不确定你提到的“许多Ruby文档”是什么,这里是最新的

Ruby解析器尽力理解并成功解析输入;当它看到语法错误时,它会尝试另一种方法。也就是说,语法错误比所有运算符优先级规则具有更高的优先级

因为LHO必须是可变的,所以它从赋值开始。在这种情况下,可以使用默认的优先顺序完成解析,并且在
*=
之前完成
+

age = 2
age *= age + 1
#⇒ 6

Ruby在代码实际执行之前有3个阶段

标记化->解析->编译

让我们看看Ruby生成的AST(抽象语法树),这是解析阶段

# @ NODE_SCOPE (line: 1, location: (1,0)-(1,12))
# | # new scope
# | # format: [nd_tbl]: local table, [nd_args]: arguments, [nd_body]: body
# +- nd_tbl (local table): :age
# +- nd_args (arguments):
# |   (null node)
# +- nd_body (body):
#     @ NODE_OPCALL (line: 1, location: (1,0)-(1,12))*
#     | # method invocation
#     | # format: [nd_recv] [nd_mid] [nd_args]
#     | # example: foo + bar
#     +- nd_mid (method id): :+
#     +- nd_recv (receiver):
#     |   @ NODE_LIT (line: 1, location: (1,0)-(1,1))
#     |   | # literal
#     |   | # format: [nd_lit]
#     |   | # example: 1, /foo/
#     |   +- nd_lit (literal): 1
#     +- nd_args (arguments):
#         @ NODE_ARRAY (line: 1, location: (1,4)-(1,12))
#         | # array constructor
#         | # format: [ [nd_head], [nd_next].. ] (length: [nd_alen])
#         | # example: [1, 2, 3]
#         +- nd_alen (length): 1
#         +- nd_head (element):
#         |   @ NODE_DASGN_CURR (line: 1, location: (1,4)-(1,12))
#         |   | # dynamic variable assignment (in current scope)
#         |   | # format: [nd_vid](current dvar) = [nd_value]
#         |   | # example: 1.times { x = foo }
#         |   +- nd_vid (local variable): :age
#         |   +- nd_value (rvalue):
#         |       @ NODE_CALL (line: 1, location: (1,4)-(1,12))
#         |       | # method invocation
#         |       | # format: [nd_recv].[nd_mid]([nd_args])
#         |       | # example: obj.foo(1)
#         |       +- nd_mid (method id): :*
#         |       +- nd_recv (receiver):
#         |       |   @ NODE_DVAR (line: 1, location: (1,4)-(1,7))
#         |       |   | # dynamic variable reference
#         |       |   | # format: [nd_vid](dvar)
#         |       |   | # example: 1.times { x = 1; x }
#         |       |   +- nd_vid (local variable): :age
#         |       +- nd_args (arguments):
#         |           @ NODE_ARRAY (line: 1, location: (1,11)-(1,12))
#         |           | # array constructor
#         |           | # format: [ [nd_head], [nd_next].. ] (length: [nd_alen])
#         |           | # example: [1, 2, 3]
#         |           +- nd_alen (length): 1
#         |           +- nd_head (element):
#         |           |   @ NODE_LIT (line: 1, location: (1,11)-(1,12))
#         |           |   | # literal
#         |           |   | # format: [nd_lit]
#         |           |   | # example: 1, /foo/
#         |           |   +- nd_lit (literal): 2
#         |           +- nd_next (next element):
#         |               (null node)
#         +- nd_next (next element):
#             (null node)
如您所见,
+-nd#mid(方法id):+
其中
1
被视为接收方,右侧的所有内容都被视为参数。现在,它更进一步,尽最大努力评估参数

进一步支持Aleksei的伟大回答。
@NODE\u DASGN\u CURR(第1行,位置:(1,4)-(1,12))
是对
age
的赋值,作为一个局部变量,因为它将其解码为
age=age*2
,这就是为什么
+-nd\u mid(方法id):*
被视为对
age
的操作作为接收器,而
2
作为其参数

现在,当它继续编译时,它尝试作为操作:
age*2
其中
age
为nil,因为它已经将其解析为没有预先赋值的局部变量,因此引发异常
未定义的方法'*',用于nil:NilClass(NoMethodError)


它的工作原理是,接收器上的任何操作都必须有来自RHO的已计算参数。

检查
ruby-y
输出,您可以准确地看到发生了什么。考虑到
1+age*=2
的来源,输出表明发生了这种情况(简化):

找到了
tINTEGER
,识别为
简单的_数值的
,它是一个
数值的
,它是一个
文字的
,它是一个
主要的
。知道接下来是
+
primary
被识别为
arg

找到了
+
。还不能交易

tIDENTIFIER
已找到。知道下一个令牌是
tOP\u ASGN
(操作员分配),
tIDENTIFIER
被识别为
user\u variable
,然后被识别为
var\u lhs

tOP\u ASGN
找到。还不能交易

tINTEGER
找到。与上一个相同,它最终被识别为
。知道下一个令牌是
\n
被识别为
arg

此时,堆栈上有
arg+var\u lhs tOP\u ASGN arg
。在此上下文中,我们将最后一个
arg
识别为
arg\u rhs
。现在,我们可以从堆栈中弹出
var\u lhs tOP\u ASGN arg\u rhs
,并将其识别为
arg
,堆栈的结尾为
arg+arg
,这可以简化为
arg

arg
然后被识别为
expr
stmt
top\u stmt
top\u stmts
<代码>\n被识别为
术语
,然后是
术语
,然后是
选择术语
<代码>顶部搜索选项术语被识别为
顶部搜索选项
,最终被识别为
程序


另一方面,如果源代码
1+age*2
,则会发生以下情况:

找到了
tINTEGER
,识别为
简单的_数值的
,它是一个
数值的
,它是一个
文字的
,它是一个
主要的
。知道接下来是
+
primary
被识别为
arg

找到了
+
。还不能交易

tIDENTIFIER
已找到。知道下一个令牌是
*
tIDENTIFIER
被识别为
user\u variable
,然后是
var\u ref
,然后是
primary
arg

*
已找到。还不能交易

tINTEGER
找到。与上一个相同,它最终被识别为
。知道下一个令牌是
\n
被识别为
arg

堆栈现在是
arg+arg*arg
arg*arg
可以减少为
arg
,结果
arg+arg
也可以减少为
arg

arg
然后被识别为
expr
stmt
top\u stmt
top\u stmts
<代码>\n被识别为
术语
,然后是
术语
,然后是
选择术语
<代码>顶部搜索选项术语被识别为
顶部搜索选项
,最终被识别为
程序


关键的区别是什么?在第一段代码中,
age
(一个
tIDENTIFIER
)被识别为
var\u lhs
(赋值的左侧),但在第二段代码中,它是
var\u ref
(变量引用)。为什么?因为Bison是一个LALR(1)解析器,这意味着它有一个令牌前瞻。所以
age
var\u lhs
,因为Ruby saw
tOP\u ASGN
即将出现;当它看到
*
时,它是
var\u ref
。这是因为Ruby知道(使用Bison生成的巨大数据)一个规范