?
This document uses PHP Chinese website manual Release
詞法解析、語法解析
這一節(jié)我們分析下PHP的解析階段,即 PHP代碼->抽象語法樹(AST) 的過程。
PHP使用re2c、bison完成這個階段的工作:
re2c: 詞法分析器,將輸入分割為一個個有意義的詞塊,稱為token
bison: 語法分析器,確定詞法分析器分割出的token是如何彼此關(guān)聯(lián)的
例如:
$a = 2 + 3;
詞法分析器將上面的語句分解為這些token:$a、=、2、+、3,接著語法分析器確定了2+3是一個表達式,而這個表達式被賦值給了a,我們可以這樣定義詞法解析規(guī)則:
/*!re2c LABEL [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]* LNUM [0-9]+ //規(guī)則 "$"{LABEL} {return T_VAR;} {LNUM} {return T_NUM;} */
然后定義語法解析規(guī)則:
//token定義%token T_VAR %token T_NUM//語法規(guī)則statement: T_VAR '=' T_NUM '+' T_NUM {ret = str2int($3) + str2int($5);printf("%d",ret);} ;
上面的語法規(guī)則只能識別兩個數(shù)值相加,假如我們希望支持更復(fù)雜的運算,比如:
$a = 3 + 4 - 6;
則可以配置遞歸規(guī)則:
//語法規(guī)則statement: T_VAR '=' expr {} ; expr: T_NUM {...} |expr '?' T_NUM {} ;
這樣將支持若干表達式,用語法分析樹表示:
接下來我們看下PHP具體的解析過程,PHP編譯階段流程:
其中 zendparse() 就是詞法、語法解析過程,這個函數(shù)實際就是bison中提供的語法解析函數(shù) yyparse() :
#define yyparse zendparse
yyparse() 不斷調(diào)用 yylex() 得到token,然后根據(jù)token匹配語法規(guī)則:
#define yylex zendlex//zend_compile.cint zendlex(zend_parser_stack_elem *elem){ zval zv; int retval; ... again: ZVAL_UNDEF(&zv); retval = lex_scan(&zv); if (EG(exception)) { //語法錯誤 return T_ERROR; } ... if (Z_TYPE(zv) != IS_UNDEF) { //如果在分割token中有zval生成則將其值復(fù)制到zend_ast_zval結(jié)構(gòu)中 elem->ast = zend_ast_create_zval(&zv); } return retval; }
這里兩個關(guān)鍵點需要注意:
(1) token值:詞法解析器解析到的token值內(nèi)容就是token值,這些值統(tǒng)一通過 zval 存儲,上面的過程中可以看到調(diào)用lex_scan參數(shù)是是個zval*,在具體的命中規(guī)則總會將解析到的token保存到這個值,從而傳遞給語法解析器使用,比如PHP中的解析變量的規(guī)則:$a;,其詞法解析規(guī)則為:
<ST_IN_SCRIPTING,ST_DOUBLE_QUOTES,ST_HEREDOC,ST_BACKQUOTE,ST_VAR_OFFSET>"$"{LABEL} { //將匹配到的token值保存在zval中 zend_copy_value(zendlval, (yytext+1), (yyleng-1)); //只保存{LABEL}內(nèi)容,不包括$,所以是yytext+1 RETURN_TOKEN(T_VARIABLE); }
zendlval就是我們傳入的zval*,yytext指向命中的token值起始位置,yyleng為token值的長度。
(2) 語義值類型:bison調(diào)用re2c分割token有兩個含義,第一個是token類型,另一個是token值,token類型一般以yylex的返回值告訴bison,而token值就是語義值,這個值一般定義為固定的類型,這個類型就是語義值類型,默認為int,可以通過 YYSTYPE 定義,而PHP中這個類型是 zend_parser_stack_elem ,這就是為什么zendlex的參數(shù)為zend_parser_stack_elem的原因。
#define YYSTYPE zend_parser_stack_elemtypedef union _zend_parser_stack_elem { zend_ast *ast; //抽象語法樹主要結(jié)構(gòu) zend_string *str; zend_ulong num; } zend_parser_stack_elem;
實際這是個union,ast類型用的比較多(其它兩種類型暫時沒發(fā)現(xiàn)有地方在用),這樣可以通過%token、%type將對應(yīng)的值修改為elem.ast,所以在zend_language_parser.y中使用的$$、$1、$2......多數(shù)都是 zend_parser_stack_elem.ast :
%token <ast> T_LNUMBER "integer number (T_LNUMBER)"%token <ast> T_DNUMBER "floating-point number (T_DNUMBER)"%token <ast> T_STRING "identifier (T_STRING)"%token <ast> T_VARIABLE "variable (T_VARIABLE)"%type <ast> top_statement namespace_name name statement function_declaration_statement %type <ast> class_declaration_statement trait_declaration_statement %type <ast> interface_declaration_statement interface_extends_list
語法解析器從start開始調(diào)用,然后層層匹配各個規(guī)則,語法解析器根據(jù)命中的語法規(guī)則創(chuàng)建AST節(jié)點,最后將生成的AST根節(jié)點賦到 CG(ast) :
%% /* Rules */start: top_statement_list { CG(ast) = $1; } ; top_statement_list: top_statement_list top_statement { $$ = zend_ast_list_add($1, $2); } | /* empty */ { $$ = zend_ast_create_list(0, ZEND_AST_STMT_LIST); } ;
首先會創(chuàng)建一個根節(jié)點list,然后將后面不斷命中top_statement生成的ast加到這個list中,zend_ast具體結(jié)構(gòu):
enum _zend_ast_kind { ZEND_AST_ZVAL = 1 << ZEND_AST_SPECIAL_SHIFT, ZEND_AST_ZNODE, /* list nodes */ ZEND_AST_ARG_LIST = 1 << ZEND_AST_IS_LIST_SHIFT, ... };struct _zend_ast { zend_ast_kind kind; /* Type of the node (ZEND_AST_* enum constant) */ zend_ast_attr attr; /* Additional attribute, use depending on node type */ uint32_t lineno; /* Line number */ zend_ast *child[1]; /* Array of children (using struct hack) */};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;
根節(jié)點實際為zend_ast_list,每條語句對應(yīng)的ast保存在child中,使用中zend_ast_list、zend_ast可以相互轉(zhuǎn)化,kind標識的是ast節(jié)點類型,后面會根據(jù)這個值生成具體的opcode,另外函數(shù)、類還會用到另外一種ast節(jié)點結(jié)構(gòu):
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; //結(jié)束行號 uint32_t flags; unsigned char *lex_pos; zend_string *doc_comment; zend_string *name; zend_ast *child[4]; //類中會將繼承的父類、實現(xiàn)的接口以及類中的語句解析保存在child中} zend_ast_decl;
這么看比較難理解,接下來我們從一個簡單的例子看下最終生成的語法樹。
$a = 123; $b = "hi~";echo $a,$b;
具體解析過程這里不再解釋,有興趣的可以翻下zend_language_parse.y中,這個過程不太容易理解,需要多領(lǐng)悟幾遍,最后生成的ast如下圖:
總結(jié):
這一節(jié)我們主要介紹了PHP詞法、語法解析生成抽象語法樹(AST)的過程,此過程是PHP語法實現(xiàn)的基礎(chǔ),也是zend引擎非常關(guān)鍵的一部分,后續(xù)介紹的內(nèi)容都是基于此過程的產(chǎn)出結(jié)果展開的。這部分內(nèi)容關(guān)鍵在于對re2c、bison的應(yīng)用上,如果是初次接觸它們可能不太容易理解,這里不再對re2c、bison作更多解釋,想要了解更多的推薦看下 《flex與bison》 這本書。