【PHP7源碼學習】2019-03-21 AST

baiyanphp

所有視頻:https://segmentfault.com/a/11...node

原視頻地址:http://replay.xesv5.com/ll/24...segmentfault

引入

  • 【PHP源碼學習】2019-03-20 PHP詞法分析這篇筆記中,咱們談到在PHP7中,會依次進行詞法分析(將代碼切成一個一個小段)、語法分析(分析代碼的語法結構)、而後會生成一顆抽象語法樹(AST)。
  • 爲何要有AST這種東西呢?由於語法分析以後所獲得的的語句或表達式爲文本類型的信息,它對人友好,可是對計算機並不友好,因此須要將其轉換成合適的數據結構,讓計算機更加方便地對其進行解析,這個數據結構就是AST。
  • 例:a = b + c,怎麼用抽象語法樹來表達?

  • 那麼使用中序遍歷就能夠獲得上述表達式。
  • 拓展:對於樹的中序遍歷,有遞歸與非遞歸兩種方式:數組

    • 遞歸中序遍歷很簡單,遞歸訪問左子樹、根節點、右子樹便可
    • 非遞歸中序遍歷:藉助棧數據結構

      - 碰到等號壓棧,而後往左子樹走
      - 將a壓棧,a沒有左右子樹,a出棧(a)
      - 等號出棧(a =)
      - 將它的右子樹加號壓棧
      - 加號有左子樹,將b壓棧
      - b沒有左右子樹,b出棧(a = b)
      - 加號出棧(a = b +)
      - 加號有右子樹c,將c壓棧
      - c沒有左右子樹,c出棧(a = b + c)
    • 樹的層序遍歷:藉助隊列,此處不展開
  • 回到AST, 那麼若是在PHP中,讓你去實現一個AST,你會怎麼實現?
  • AST結點的設計初版:
struct Tree {
    char type           //結點類型,表示是運算符仍是操做數
    Tree *left           //左孩子
    Tree *right        //右孩子
}
  • 存在的問題:由於不是全部表達式都是二元表達式,故不能夠所有都用二叉樹表示(如foreach循環中foreach($a as $k => $v)至少需3個結點來表示$a/$k/$v等詞法單元),故須要在此基礎上作一些擴展。
  • 第二版:
struct Tree {
    char type           //結點類型,表示是運算符仍是操做數
    int children        //有多少個孩子
    Tree child[]        //用一個數組存儲全部孩子
}

PHP中AST的重要結構與概念

struct _zend_ast {
    zend_ast_kind kind; /* 結點類型,至關於咱們上面的type */
    zend_ast_attr attr; /* 先忽略 */
    uint32_t lineno;    /* 行號(進行語法分析的時候須要記錄代碼所在行號) */
    zend_ast *child[1]; /* 柔性數組,存儲孩子結點*/
};
  • 這樣一看,好像並無存儲有多少個孩子的字段。注意這個kind字段,它是zend_ast_kind類型,看下這個類型的定義:
typedef uint16_t zend_ast_kind;
  • 它是一個uint16類型,足以存儲全部類型了,那麼具體有哪些類型呢?
  • 在PHP中,AST的結點是按照以下代碼所示分類存儲的,這些分類用枚舉類型來表示:
enum _zend_ast_kind {
    /* special nodes 特殊類型結點*/
    ZEND_AST_ZVAL = 1 << ZEND_AST_SPECIAL_SHIFT, (1 << 6 = 64)
    ZEND_AST_ZNODE, (65)

    /* declaration nodes 定義類型結點*/
    ZEND_AST_FUNC_DECL,
    ZEND_AST_CLOSURE,
    ZEND_AST_METHOD,
    ZEND_AST_CLASS,

    /* list nodes LIST類型結點*/
    ZEND_AST_ARG_LIST = 1 << ZEND_AST_IS_LIST_SHIFT,(1 << 7 = 128)
    ZEND_AST_ARRAY,
    ZEND_AST_ENCAPS_LIST,
    ZEND_AST_EXPR_LIST,
    ZEND_AST_STMT_LIST,
    ZEND_AST_IF,
    ZEND_AST_SWITCH_LIST,
    ZEND_AST_CATCH_LIST,
    ZEND_AST_PARAM_LIST,
    ZEND_AST_CLOSURE_USES,
    ZEND_AST_PROP_DECL,
    ZEND_AST_CONST_DECL,
    ZEND_AST_CLASS_CONST_DECL,
    ZEND_AST_NAME_LIST,
    ZEND_AST_TRAIT_ADAPTATIONS,
    ZEND_AST_USE,

    /* 0 child nodes 0個孩子結點*/
    ZEND_AST_MAGIC_CONST = 0 << ZEND_AST_NUM_CHILDREN_SHIFT,(0 << 8 = 0)
    ZEND_AST_TYPE,

    /* 1 child node 1個孩子結點*/
    ZEND_AST_VAR = 1 << ZEND_AST_NUM_CHILDREN_SHIFT,(1 << 8 = 256)
    ZEND_AST_CONST,
    ZEND_AST_UNPACK,
    ZEND_AST_UNARY_PLUS,
    ZEND_AST_UNARY_MINUS,
    ZEND_AST_CAST,
    ZEND_AST_EMPTY,
    ZEND_AST_ISSET,
    ZEND_AST_SILENCE,
    ZEND_AST_SHELL_EXEC,
    ZEND_AST_CLONE,
    ZEND_AST_EXIT,
    ZEND_AST_PRINT,
    ZEND_AST_INCLUDE_OR_EVAL,
    ZEND_AST_UNARY_OP,
    ZEND_AST_PRE_INC,
    ZEND_AST_PRE_DEC,
    ZEND_AST_POST_INC,
    ZEND_AST_POST_DEC,
    ZEND_AST_YIELD_FROM,

    ZEND_AST_GLOBAL,
    ZEND_AST_UNSET,
    ZEND_AST_RETURN,
    ZEND_AST_LABEL,
    ZEND_AST_REF,
    ZEND_AST_HALT_COMPILER,
    ZEND_AST_ECHO,
    ZEND_AST_THROW,
    ZEND_AST_GOTO,
    ZEND_AST_BREAK,
    ZEND_AST_CONTINUE,

    /* 2 child nodes 2個孩子結點*/
    ZEND_AST_DIM = 2 << ZEND_AST_NUM_CHILDREN_SHIFT,(2 << 8 = 512)
    ZEND_AST_PROP,
    ZEND_AST_STATIC_PROP,
    ZEND_AST_CALL,
    ZEND_AST_CLASS_CONST,
    ZEND_AST_ASSIGN,
    ZEND_AST_ASSIGN_REF,
    ZEND_AST_ASSIGN_OP,
    ZEND_AST_BINARY_OP,
    ZEND_AST_GREATER,
    ZEND_AST_GREATER_EQUAL,
    ZEND_AST_AND,
    ZEND_AST_OR,
    ZEND_AST_ARRAY_ELEM,
    ZEND_AST_NEW,
    ZEND_AST_INSTANCEOF,
    ZEND_AST_YIELD,
    ZEND_AST_COALESCE,

    ZEND_AST_STATIC,
    ZEND_AST_WHILE,
    ZEND_AST_DO_WHILE,
    ZEND_AST_IF_ELEM,
    ZEND_AST_SWITCH,
    ZEND_AST_SWITCH_CASE,
    ZEND_AST_DECLARE,
    ZEND_AST_USE_TRAIT,
    ZEND_AST_TRAIT_PRECEDENCE,
    ZEND_AST_METHOD_REFERENCE,
    ZEND_AST_NAMESPACE,
    ZEND_AST_USE_ELEM,
    ZEND_AST_TRAIT_ALIAS,
    ZEND_AST_GROUP_USE,

    /* 3 child nodes 3個孩子結點*/
    ZEND_AST_METHOD_CALL = 3 << ZEND_AST_NUM_CHILDREN_SHIFT,
    ZEND_AST_STATIC_CALL,
    ZEND_AST_CONDITIONAL,

    ZEND_AST_TRY,
    ZEND_AST_CATCH,
    ZEND_AST_PARAM,
    ZEND_AST_PROP_ELEM,
    ZEND_AST_CONST_ELEM,

    /* 4 child nodes 4個孩子結點*/
    ZEND_AST_FOR = 4 << ZEND_AST_NUM_CHILDREN_SHIFT,
    ZEND_AST_FOREACH,
};
  • 先忽略上面特殊節點、定義結點和LIST類型結點這幾個類型,主要關注下面0個子結點、1個子結點的類型,這樣,咱們根據_zend_ast中存儲的kind數值,再對照這個類型表,就能夠知道它有幾個子結點了。
  • 咱們再看LIST類型,它是怎麼存儲子結點數量的呢?會轉成一個專門的結構體用來存儲LIST類型的結點:
/* Same as zend_ast, but with children count, which is updated dynamically */
/*與zend_ast結點的功能相同可是多了一個子結點的計數,它會被動態地更新*/
typedef struct _zend_ast_list {
    zend_ast_kind kind;
    zend_ast_attr attr;
    uint32_t lineno;
    uint32_t children;
    zend_ast *child[1];
} zend_ast_list;
  • 咱們看這個結構體,除了uint32_t類型的children子結點計數字段,其他與咱們上述zend_ast結構體一摸同樣。
  • 這樣,在基本的zend_ast結構體中,kind字段只須要存一個數字,表明它是什麼類型,就能夠直接得出是LIST類型(孩子結點個數存在對應的zend_ast_list結構體中)有0個、1個、2個孩子結點等等。
  • 再關注特殊類型中的ZEND_AST_ZVAL類型,它表明AST中結點變量或者常量的(如變量$a的值就爲字符串"a",常量1的值爲1,均存在這個zval中,下文會講)
/* Lineno is stored in val.u2.lineno */
/* Lineno 字段存儲在zval中的 val.u2.lineno中 */
typedef struct _zend_ast_zval {
    zend_ast_kind kind;
    zend_ast_attr attr;
    zval val;
} zend_ast_zval;
  • 最後剩下的就是定義類型,包括類、函數等定義,會轉成以下結構存儲定義類型的信息:
/* Separate structure for function and class declaration, as they need extra information. */
/* 爲函數和類設計的特殊結構,它們須要額外的描述信息 */
typedef struct _zend_ast_decl {
    zend_ast_kind kind;
    zend_ast_attr attr; /* Unused - for structure compatibility */
    uint32_t start_lineno; //類和函數是一個區間,因此記錄開始行號和結束行號
    uint32_t end_lineno;
    uint32_t flags;
    unsigned char *lex_pos;
    zend_string *doc_comment;
    zend_string *name;
    zend_ast *child[4];
} zend_ast_decl;

PHP中AST實現示例

簡單的賦值與表達式示例

  • 咱們看下面一段PHP代碼,看下它的AST結構是什麼樣的:
<?php
$a = 1;
$b = $a + 2;
  • 利用gdb調試這段代碼,並在zend_compile處打一個斷點。這裏會進行詞法分析和語法分析(注意詞法分析和語法分析是同時執行的,解析出一個token就開始進行語法分析,而不是串行執行的),並查看compiler_globals.ast字段,這個字段就是生成的抽象語法樹指針了,咱們打印它的內容:

  • 咱們看到這裏的kind的值爲132,對應 ZEND_AST_STMT_LIST(表達式)類型,屬於LIST這個大類,它就是咱們的根節點了。然而LIST是專門用zend_ast_list結構體來表示的,因此咱們強轉一下:

  • 能夠看到,它有兩個孩子結點,發現兩個孩子的kind值均爲517,即ZEND_AST_ASSIGN(賦值)類型。這裏咱們選擇第一個kind值爲517的結點,它屬於2 child nodes大類,說明它又有兩個孩子結點,打印它的第一個孩子結點(後面會打印第二個孩子結點),咱們按照這一個結點深度優先去調試:

  • 它的kind值是256,表明ZEND_AST_VAR(變量)類型,屬於1 child nodes大類,那麼咱們繼續深度優先打印它的惟一一個孩子結點:

  • 它的kind值是64,表明ZEND_AST_ZVAL類型,屬於特殊類型。上面咱們講過,PHP使用一個zend_ast_zval結構體來專門保存這種類型,強轉一下:

  • 它的kind值是64,zval字段中能夠看到type值是6(字符串類型),咱們深刻看一下這個zend_string的內容:

  • 能夠看到它的字符串值是」a「
  • 回到上面第二個加粗的部分,咱們此次打印第二個孩子結點:

  • 它的kind值也是64,屬於ZEND_AST_ZVAL類型,一樣將其強轉一下:

  • 咱們發現,它的type值是4(整型),那麼咱們直接看zval中的lval字段,值爲1,說明它直接存儲的是$a = 1;這個表達式中的常量值1
  • 那麼咱們畫出如今的AST結構圖(AST結點以」類型(值)「的形式表示,下同):

  • 目前爲止,第二個kind值爲517類型的結點咱們尚未看,咱們繼續往下走:

  • 咱們發現它的kind值是256,也是一個ZEND_AST_VAR(變量)類型,它有一個孩子結點,打印:

  • 一樣地,它的惟一一個孩子結點也是一個ZEND_AST_ZVAL類型,強轉並打印其中的zval字段,發現其type是字符串類型,咱們繼續打印該字符串的內容:

  • 能夠看到它的字符串值是」b「
  • 咱們能夠畫出當前AST的結構:

  • 而後繼續打印kind爲517的第二個孩子結點:

  • 它的kind是520,查表獲得它是ZEND_AST_BINARY_OP類型,它也屬於2 child nodes大類,有兩個孩子結點,它表明二元操做(加減乘除等)。因此到底表示加減乘除中的哪個呢,這時候須要它的attr字段來細化究竟是哪一種運算,這裏attr = 1表明加法運算。那麼咱們繼續打印其中一個孩子結點

  • 它的子結點是256,即ZEND_AST_VAR(變量)類型,打印其惟一一個孩子結點,仍爲ZEND_AST_ZVAL類型,強轉並打印其內容爲」a「
  • 咱們看kind是520的第二個孩子結點

  • 咱們發現它就是一個ZEND_AST_ZVAL類型,值爲2
  • 那麼咱們能夠畫出完整的AST:

  • 那麼經過中序遍歷,就能夠獲得最終的代碼

帶括號表達式示例

  • 咱們看下面一段帶括號的PHP表達式代碼,看下它的AST結構:
<?php
$a = (1+2)*3;

  • 咱們利用和上方同樣的方式,訪問它的根節點,發現也是一個LIST類型,有1個子結點,這個子節點的類型是ZEND_AST_ASSIGN,它有2個孩子結點,咱們打印其中一個孩子結點

  • 它的類型是ZEND_AST_VAR類型,有一個孩子結點,繼續打印:

  • 它的類型也是一個ZEND_AST_ZVAL類型,其類型是字符串,值爲a
  • 那麼能夠畫出當前AST的結構:

  • 回到517(ZEND_AST_ASSIGN)的另外一個孩子結點,觀察它的值:

  • 能夠看到,它是ZEND_AST_BINARY_OP類型,attr值爲3,表明*,它有2個孩子結點,將他們所有打印出來:

  • 能夠看到,第一個孩子是一個ZEND_AST_BINARY_OP類型,表明+,它也有兩個孩子結點,分別爲ZEND_AST_ZVAL,值爲1和2,而第二個孩子就是值爲3的ZEND_AST_ZVAL。這裏沒有表示括號的結點,是由於AST已經表示了該表達式的優先級,因此無需額外存儲,如今咱們能夠畫出AST的完整結構:

  • 視頻中還有foreach的案例,限於篇幅再也不貼圖,其分析方法都是相似的,只是多了幾個新的類型
  • 在PHP中,任何代碼均可以被轉成惟一的一個AST,根節點每每都是132(LIST)類型
相關文章
相關標籤/搜索