何时解析一行Ruby?评价?被执行?

时间:2019-07-02 23:24:51

标签: ruby abstract-syntax-tree internals

即使person不存在,我也惊讶地发现params[:person_id]是由以下代码行定义的:

person = Person.find(params[:person_id]) if params[:person_id]

我有点期望Ruby首先检查if语句,然后才定义person。实际上,似乎person的定义早于nil,但仍然是irb> foo # NameError (undefined local variable or method `foo' for main:Object) irb> if false irb> foo = 'bar' irb> end irb> foo # => nil

在调查中,我尝试了以下操作:

foo

最初if是未定义的。但是,即使仅在未评估的foo块中引用了它,也对其进行了定义。

我现在猜测整个程序都已解析(?),并且nil节点已添加到抽象语法树(即已定义)中。然后执行程序(?),但是跳过了该行(未求值(?)),因此foo为public float[] rectangleToVerts(float x, float y, float width, float height) float[] verts = new float[8]; verts[0] = x - width / 2; verts[1] = y + height / 2; verts[2] = x + width / 2; verts[3] = y + height / 2; verts[4] = x + width / 2; verts[5] = y - height / 2; verts[6] = x - width / 2; verts[7] = y - height / 2; return verts; } public void tick(){ shape.setVertices(rectangleToVerts(x, y, 5, 5); } (已定义但未设置为值)。

我不确定如何确认或驳斥这种预感。如何学习和挖掘Ruby的内部原理,并发现在这种特定情况下会发生什么?

1 个答案:

答案 0 :(得分:0)

回答我自己的问题,将Jayanswer to a similar question链接到section of the docs并在其中进行解释:

  

解析器遇到分配时创建本地变量,而不是分配发生时创建

Ruby Hacking Guide中对此有更深入的分析(没有可用的部分链接,搜索或滚动到“局部变量定义”部分):

  

顺便说一句,它是在“它出现”时定义的,这意味着即使未分配它也已定义。定义的[但尚未分配]变量的初始值为nil。

这回答了最初的问题,但没有回答如何了解更多信息。


Jay和simonwo都由Pat Shaughnessy推荐Ruby Under a Microscope,我很想读。

此外,《 Ruby Hacking指南》的其余部分涵盖了很多细节,并实际检查了基础C代码。 ObjectsParser两章与最初有关变量分配的问题特别相关(与Variables and constants一章无关,它只是使您回到Objects一章)。

我还发现Parser gem是查看解析器如何工作的有用工具。安装(gem install parser后,您可以开始检查不同的代码位,以查看解析器对它们的作用。

该gem还捆绑了ruby-parse实用程序,该实用程序使您可以检查Ruby解析不同代码段的方式。 -E-L选项对我们来说最有趣,如果我们只想处理Ruby片段,例如-e,则foo = 'bar'选项是必需的。例如:

> ruby-parse -E -e "foo = 'bar'"
foo = 'bar'   
^~~ tIDENTIFIER "foo"                           expr_cmdarg  [0 <= cond] [0 <= cmdarg] 
foo = 'bar'   
    ^ tEQL "="                                  expr_beg     [0 <= cond] [0 <= cmdarg] 
foo = 'bar'   
      ^~~~~ tSTRING "bar"                       expr_end     [0 <= cond] [0 <= cmdarg] 
foo = 'bar'   
           ^ false "$eof"                       expr_end     [0 <= cond] [0 <= cmdarg] 
(lvasgn :foo
  (str "bar"))
ruby-parse -L -e "foo = 'bar'"
s(:lvasgn, :foo,
  s(:str, "bar"))
foo = 'bar'
~~~ name      
    ~ operator        
~~~~~~~~~~~ expression
s(:str, "bar")
foo = 'bar'
          ~ end
      ~ begin         
      ~~~~~ expression

链接到顶部的两个参考都突出显示了一个边缘情况。 Ruby docs使用示例p a if a = 0.zero?,而Ruby Hacking Guide使用等效示例p(lvar) if lvar = true,两者都引发NameError

旁注::请记住,=表示分配,==表示比较。边缘情况下的if foo = true构造告诉Ruby检查表达式foo = true的计算结果是否为true。换句话说,它将值true分配给foo,然后检查分配结果是否为true(将是)。这很容易与更常见的if foo == true混淆,后者仅检查footrue的比较是否相等。由于两者很容易混淆,因此如果我们在条件warning: found `= literal' in conditional, should be ==中使用赋值运算符,Ruby将发出警告。

使用ruby-parse实用程序,将原始示例foo = 'bar' if false与边缘情况foo if foo = true进行比较:

> ruby-parse -L -e "foo = 'bar' if false"
s(:if,
  s(:false),
  s(:lvasgn, :foo,
    s(:str, "bar")), nil)
foo = 'bar' if false
            ~~ keyword         
~~~~~~~~~~~~~~~~~~~~ expression
s(:false)
foo = 'bar' if false
               ~~~~~ expression
s(:lvasgn, :foo,
  s(:str, "bar"))
foo = 'bar' if false     # Line 13
~~~ name                 # <-- `foo` is a name
    ~ operator        
~~~~~~~~~~~ expression
s(:str, "bar")
foo = 'bar' if false
          ~ end
      ~ begin         
      ~~~~~ expression

正如您在上面的输出的第13和14行上看到的那样,在原始示例中,foo是一个名称(即变量)。

> ruby-parse -L -e "foo if foo = true"
s(:if,
  s(:lvasgn, :foo,
    s(:true)),
  s(:send, nil, :foo), nil)
foo if foo = true
    ~~ keyword              
~~~~~~~~~~~~~~~~~ expression
s(:lvasgn, :foo,
  s(:true))
foo if foo = true         # Line 10
       ~~~ name           # <-- `foo` is a name
           ~ operator       
       ~~~~~~~~~~ expression
s(:true)
foo if foo = true
             ~~~~ expression
s(:send, nil, :foo)
foo if foo = true         # Line 18
~~~ selector              # <-- `foo` is a selector
~~~ expression

在边缘示例中,第二个foo也是变量(第10和11行),但是当我们看第18和19行时,我们看到第一个foo被标识为选择器(即方法)。


这表明解析器决定事物是方法还是变量,并以与稍后进行评估的顺序不同的顺序解析行。

考虑边缘情况...

解析器运行时:

  • 首先将整行视为单个表达式
  • 然后将其分解为两个用if关键字分隔的表达式
  • 第一个表达式foo以小写字母开头,因此它必须是方法或变量。它不是现有变量,并且没有赋值运算符,因此解析器认为它必须是方法
  • 第二个表达式foo = true被分解为表达式,运算符,表达式。同样,表达式foo也以小写字母开头,因此它必须是方法或变量。它不是现有变量,但是后面是赋值运算符,因此解析器知道将其添加到局部变量列表中。

稍后评估者运行时:

  • 它将首先将true分配给foo
  • 然后它将执行条件并检查该赋值的结果是否为真(在这种情况下为真)
  • 然后它将调用foo方法(除非我们使用NameError处理,否则它将引发method_missing)。