文档章节

php 词法分析,语法分析

o
 osc_y8yehimr
发布于 2019/03/22 11:12
字数 2556
阅读 12
收藏 0

精选30+云产品,助力企业轻松上云!>>>

  php的词法分析 可以理解为 通过一定的规则,把输入的代码 区分出哪些是 是$开头的变量, 哪些是 以两个单引号括起来的字符串,哪些是以两个双引号括起来的字符串 等等, 这些区分出来的东西 称为token ,token 之间的联系 是由语法分析来完成的, 比如 赋值,加减乘除;

  

  语法分析详见这里

  语法分析的驱动程序 yyparse() 调用yylex()这个函数 , 这个函数 可以由flex生成,也可以人为编写,在php中,属于后者;

每次执行yylex()函数,会返回一个token, 每个token都会有类型和相应的值 , 类型一般在zend_language_parse.y中的%token表示,这些类型其实是数值,存放于zend_language_parse.h

  token的值通过yylval供bison使用,在php中其实是zendlval,zendlval的类型是一个zval的结构体

#define YYSTYPE zval

YYSTYPE yylval;

#define yylval  zendlval

  把分析好的yytext , 通过zend_copy_value将其拷贝到zval这个结构体中去

    zend_language_parse.y中的token 例如下面的

%token T_INCLUDE      "include (T_INCLUDE)"

  bison官网有一段关于%token的使用的描述:

  yylex函数其实就是zendlex函数

 

这个yyparse会循环调用lex_scan,lex_scan定义在zend_language_scanning.l中 , 这个函数体大部分是一些正则表达式,以区分出代码中哪些是变量,哪些是字符串,哪些是数组等等

  

int lex_scan(zval *zendlval TSRMLS_DC)
{

....

}

 

下面举例说明:

 

<?php
$name='taek';
?>

 

下面的关于上面 $name='taek'; 的巴斯科范式

start:
top_statement_list { zend_do_end_compilation(TSRMLS_C); }
;

top_statement_list:
top_statement_list { zend_do_extended_info(TSRMLS_C); } top_statement { HANDLE_INTERACTIVE(); }
| /* empty */
;

top_statement:
statement
;

statement:
unticked_statement { DO_TICKS(); }
;

unticked_statement:
| expr ';' { zend_do_free(&$1 TSRMLS_CC); }
;
expr:
r_variable { $$ = $1; }
| expr_without_variable { $$ = $1; }
;

expr_without_variable:
| variable '=' expr { zend_check_writable_variable(&$1); zend_do_assign(&$$, &$1, &$3 TSRMLS_CC); }
| scalar { $$ = $1; }
| expr '.' expr { zend_do_binary_op(ZEND_CONCAT, &$$, &$1, &$3 TSRMLS_CC); }
;

scalar:
| common_scalar { $$ = $1; }
;

common_scalar:
T_LNUMBER { $$ = $1; }
| T_DNUMBER { $$ = $1; }
| T_CONSTANT_ENCAPSED_STRING { $$ = $1; }
;


variable:
| base_variable_with_function_calls { $$ = $1; }
;

base_variable_with_function_calls:
base_variable { $$ = $1; }
;

base_variable:
reference_variable { $$ = $1; $$.EA = ZEND_PARSED_VARIABLE; }
;

reference_variable:
| compound_variable { zend_do_begin_variable_parse(TSRMLS_C); fetch_simple_variable(&$$, &$1, 1 TSRMLS_CC); }
;


compound_variable:
T_VARIABLE { $$ = $1; }
| '$' '{' expr '}' { $$ = $3; }
;

 

  1)发现第一个字符(这里先不考虑空白)为$开头,第二个字符为n ,符合 LABEL这个正则,接着继续扫描,直到= ,使用下面的规则 将变量名存储到zendlval中

 

<ST_IN_SCRIPTING,ST_DOUBLE_QUOTES,ST_HEREDOC,ST_BACKQUOTE,ST_VAR_OFFSET>"$"{LABEL} {
    zend_copy_value(zendlval, (yytext+1), (yyleng-1));
    zendlval->type = IS_STRING;
    return T_VARIABLE;
}

 

  返回token的类型是 T_VARIABLE , 值为$name放入zendlval中,yylex()返回的yychar就是这个T_VARIABLE,经过处理,分析器经过yytable的返回,这是一个移进操作,便将T_VARIABLE 放入状态栈,$name放入符号栈 , yyparse()继续执行yylex()函数,因为这里返回的是T_VARIABLE,经过yytable查找,发现是归约操作,即把$name替换为

  注意:LABEL 是一个正则表达式  LABEL [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*  其中 \x7f-\xff 这个是识别utf8下面汉字的16进制,也就 是说,php的变量名称里面可以有汉字

<?php
$我的名字叫='taek';
echo $我的名字叫;

   上面的代码是可以正常运行的,估计是易语言出现后,才增加的功能吧

 

   2)扫描到=这个符号

<ST_IN_SCRIPTING>{TOKENS} {
    return yytext[0];
}

 此时的yychar接收 = 这个符号的ASICII的值,并放入状态栈,且将空值放入符号栈

  这里面的TOKENS也是一个正则表达式  TOKENS [;:,.\[\]()|^&+-/*=%!~$<>?@],[]括号里的字符只允许出现一次,正好=这个符号在里面,正好匹配

  

  3)接着扫描到', 这个单引号

 1 <ST_IN_SCRIPTING>b?['] {
 2     register char *s, *t;
 3     char *end;
 4     int bprefix = (yytext[0] != '\'') ? 1 : 0;
 5 
 6     while (1) {
 7         if (YYCURSOR < YYLIMIT) {
 8             if (*YYCURSOR == '\'') {
 9                 YYCURSOR++;
10                 yyleng = YYCURSOR - SCNG(yy_text);
11 
12                 break;
13             } else if (*YYCURSOR++ == '\\' && YYCURSOR < YYLIMIT) {
14                 YYCURSOR++;
15             }
16         } else {
17             yyleng = YYLIMIT - SCNG(yy_text);
18 
19             /* Unclosed single quotes; treat similar to double quotes, but without a separate token
20              * for ' (unrecognized by parser), instead of old flex fallback to "Unexpected character..."
21              * rule, which continued in ST_IN_SCRIPTING state after the quote */
22             return T_ENCAPSED_AND_WHITESPACE;
23         }
24     }
25 
26     zendlval->value.str.val = estrndup(yytext+bprefix+1, yyleng-bprefix-2);
27     zendlval->value.str.len = yyleng-bprefix-2;
28     zendlval->type = IS_STRING;
29 
30     /* convert escape sequences */
31     s = t = zendlval->value.str.val;
32     end = s+zendlval->value.str.len;
33     while (s<end) {
34         if (*s=='\\') {
35             s++;
36 
37             switch(*s) {
38                 case '\\':
39                 case '\'':
40                     *t++ = *s;
41                     zendlval->value.str.len--;
42                     break;
43                 default:
44                     *t++ = '\\';
45                     *t++ = *s;
46                     break;
47             }
48         } else {
49             *t++ = *s;
50         }
51 
52         if (*s == '\n' || (*s == '\r' && (*(s+1) != '\n'))) {
53             CG(zend_lineno)++;
54         }
55         s++;
56     }
57     *t = 0;
58 
59     if (SCNG(output_filter)) {
60         size_t sz = 0;
61         s = zendlval->value.str.val;
62         SCNG(output_filter)((unsigned char **)&(zendlval->value.str.val), &sz, (unsigned char *)s, (size_t)zendlval->value.str.len TSRMLS_CC);
63         zendlval->value.str.len = sz;
64         efree(s);
65     }
66     return T_CONSTANT_ENCAPSED_STRING;
67 }

 

  上面的YYCURSOR会随时变化的,当遇到 第二个单引号时,它会认为扫描过的符号就是字符串了,注意:如果扫描到\'时,会自动跨过,要不然后面的字符会被截掉,最终把字符串放到zendlval中,并返回TOKEN T_CONSTANT_ENCAPSED_STRING

 

<ST_IN_SCRIPTING>b?['] {
    register char *s, *t;
    char *end;
    int bprefix = (yytext[0] != '\'') ? 1 : 0;

    while (1) {
        if (YYCURSOR < YYLIMIT) {
            if (*YYCURSOR == '\'') {
                YYCURSOR++;
                yyleng = YYCURSOR - SCNG(yy_text);

                break;
            } else if (*YYCURSOR++ == '\\' && YYCURSOR < YYLIMIT) {
                YYCURSOR++;
            }
        } else {
            yyleng = YYLIMIT - SCNG(yy_text);

            /* Unclosed single quotes; treat similar to double quotes, but without a separate token
             * for ' (unrecognized by parser), instead of old flex fallback to "Unexpected character..."
             * rule, which continued in ST_IN_SCRIPTING state after the quote */
            return T_ENCAPSED_AND_WHITESPACE;
        }
    }

    zendlval->value.str.val = estrndup(yytext+bprefix+1, yyleng-bprefix-2);
    zendlval->value.str.len = yyleng-bprefix-2;
    zendlval->type = IS_STRING;

    /* convert escape sequences */
    s = t = zendlval->value.str.val;
    end = s+zendlval->value.str.len;
    while (s<end) {
        if (*s=='\\') {
            s++;

            switch(*s) {
                case '\\':
                case '\'':
                    *t++ = *s;
                    zendlval->value.str.len--;
                    break;
                default:
                    *t++ = '\\';
                    *t++ = *s;
                    break;
            }
        } else {
            *t++ = *s;
        }

        if (*s == '\n' || (*s == '\r' && (*(s+1) != '\n'))) {
            CG(zend_lineno)++;
        }
        s++;
    }
    *t = 0;

    if (SCNG(output_filter)) {
        size_t sz = 0;
        s = zendlval->value.str.val;
        SCNG(output_filter)((unsigned char **)&(zendlval->value.str.val), &sz, (unsigned char *)s, (size_t)zendlval->value.str.len TSRMLS_CC);
        zendlval->value.str.len = sz;
        efree(s);
    }
    return T_CONSTANT_ENCAPSED_STRING;
}
View Code

 

 

<ST_IN_SCRIPTING>b?['] { 中的b ,全称为binary ,即可定义 二进制的字符串,见下图

 

 

 

我在php 5.4.12 版本中 ,是可以运行的,不像上面黑框中所说的effect on of PHP 6.0.0, 但不知道什么情况下会定义一个二进制的字符串?

 

if (*YYCURSOR == '\'') {
YYCURSOR++;
yyleng = YYCURSOR - SCNG(yy_text);

break;
}

这段代码的意思是 如果 碰上第二个单引号,那么跳过,不然这就意味着结束了

 1)yylex扫描 $name后,yychar 值为 T_VARIABLE 这个token , 经过yytable运算,执行移进操作,将得到的状态值放入状态栈,同时将$name放入符号栈

  然后根据 产生式

  compound_variable:
  |  T_VARIABLE { $$ = $1; }
  | '$' '{' expr '}' { $$ = $3; }
  ;

  进行归约,先把状态栈pop出一个元素,也就是T_VARIABLE,接着符号栈pop出一个元素,也就是$name, 最后yygogo这个数组计算出新的状态,这个状态就是上面产生式的编号,将此编号放入着状态栈,$name再入符号栈

  接着经过下列几个产生式的归约

  reference_variable:
  | compound_variable { zend_do_begin_variable_parse(TSRMLS_C); fetch_simple_variable(&$$, &$1, 1 TSRMLS_CC); }
  ;

  base_variable:
    reference_variable { $$ = $1; $$.EA = ZEND_PARSED_VARIABLE; }
  ;

  base_variable_with_function_calls:
    base_variable { $$ = $1; }
  ;

  variable:

  | base_variable_with_function_calls { $$ = $1; }
  ;

  最终将variable这个产生式的编号入状态栈   

 4)yylex扫描'taek'后,yychar 的值为T_CONSTANT_ENCAPSED_STRING 这个token,经yytable数组计算出的值,执行移进操作,此值入状态栈 , 将'taek' 放入 符号栈 ,程序又回到yybackup:执行这里的代码

yyn = yypact[yystate];
if (yyn == YYPACT_NINF)
goto yydefault;

yydefault:
yyn = yydefact[yystate];
if (yyn == 0)
goto yyerrlab;
goto yyreduce;

如果yyn不为0,则由产生式

common_scalar:
T_LNUMBER { $$ = $1; }
| T_DNUMBER { $$ = $1; }
| T_CONSTANT_ENCAPSED_STRING { $$ = $1; }
;

进行归约,将状态栈,符号栈pop出若干元素,再将该产生式编号 放入状态栈,'taek'经处理(可能)后,再放入符合栈,接着再次进行归约,通过下面几个产生式

expr:
r_variable { $$ = $1; }
| expr_without_variable { $$ = $1; }
;

expr_without_variable:
| variable '=' expr { zend_check_writable_variable(&$1); zend_do_assign(&$$, &$1, &$3 TSRMLS_CC); }
| scalar { $$ = $1; }
| expr '.' expr { zend_do_binary_op(ZEND_CONCAT, &$$, &$1, &$3 TSRMLS_CC); }
;

scalar:
| common_scalar { $$ = $1; }
;

进行归约,最终将expr这个并产生式的编号入状态栈, 至此'taek'归约成功

 

5)yylex扫描 . 这个连接符,并将其ASICII返回给zendparse中的yychar, 执行移进操作, 将ASICII值放入状态栈中,符号栈放一个空元素

6)  yylex接着扫描 'world' ,流程跟4一样,符号栈中的'world',在状态栈 对应的是expr(数字而已)

7)再次归约,也就是说expr . expr  可由产生式

expr_without_variable:
| variable '=' expr { zend_check_writable_variable(&$1); zend_do_assign(&$$, &$1, &$3 TSRMLS_CC); }
| scalar { $$ = $1; }
| expr '.' expr { zend_do_binary_op(ZEND_CONCAT, &$$, &$1, &$3 TSRMLS_CC); }
;

进行归约,状态栈,符号栈 pop出三个元素, expr_without_variable 这个产生式

8)  variable = expr ,由产生式

  expr_without_variable:
  | variable '=' expr { zend_check_writable_variable(&$1); zend_do_assign(&$$, &$1, &$3 TSRMLS_CC); }
  | scalar { $$ = $1; }
  | expr '.' expr { zend_do_binary_op(ZEND_CONCAT, &$$, &$1, &$3 TSRMLS_CC); }
  ;

  进行归约,将状态栈,符号栈pop出若干元素, 再将该产生式编号入状态栈,同时将yylval入符号栈,

8) yylex扫描到;这个符号,将其ASICII值传给yychar, 入状态栈,符号栈入一个空元素

9)expr ; 由产生式

unticked_statement:
| expr ';' { zend_do_free(&$1 TSRMLS_CC); }
;

进行归约,状态栈,符合栈pop出若干个元素, 再将该产生式编号 放入状态栈,同时将

8)

common_scalar

 

 4) 接着是分号的asicII进入状态栈,空值进入符号栈, 此时进行归约操作,并产生opcode代码,

 5) 结束

双引号:

<ST_IN_SCRIPTING>b?["] {
    int bprefix = (yytext[0] != '"') ? 1 : 0;

    while (YYCURSOR < YYLIMIT) {
        switch (*YYCURSOR++) {
            case '"':
                yyleng = YYCURSOR - SCNG(yy_text);
                zend_scan_escape_string(zendlval, yytext+bprefix+1, yyleng-bprefix-2, '"' TSRMLS_CC);
                return T_CONSTANT_ENCAPSED_STRING;
            case '$':
                if (IS_LABEL_START(*YYCURSOR) || *YYCURSOR == '{') {
                    break;
                }
                continue;
            case '{':
                if (*YYCURSOR == '$') {
                    break;
                }
                continue;
            case '\\':
                if (YYCURSOR < YYLIMIT) {
                    YYCURSOR++;
                }
                /* fall through */
            default:
                continue;
        }

        YYCURSOR--;
        break;
    }

    /* Remember how much was scanned to save rescanning */
    SET_DOUBLE_QUOTES_SCANNED_LENGTH(YYCURSOR - SCNG(yy_text) - yyleng);

    YYCURSOR = SCNG(yy_text) + yyleng;

    BEGIN(ST_DOUBLE_QUOTES);
    return '"';
}

 

 

 

 

对于双引号括起来的字符串,会对里面的字符进行判断

1)发现 $符号,记录已扫描过字符的位置,然后设置新条件:BEGIN(ST_DOUBLE_QUOTES);可以理解为在ST_DOUBLE_QUOTES的规则中寻找一个恰当的规则 ,接着 继续 执行lex_scan,最终找到的是

  

<ST_DOUBLE_QUOTES>{ANY_CHAR} {
    if (GET_DOUBLE_QUOTES_SCANNED_LENGTH()) {
        YYCURSOR += GET_DOUBLE_QUOTES_SCANNED_LENGTH() - 1;
        SET_DOUBLE_QUOTES_SCANNED_LENGTH(0);

        goto double_quotes_scan_done;
    }

    if (YYCURSOR > YYLIMIT) {
        return 0;
    }
    if (yytext[0] == '\\' && YYCURSOR < YYLIMIT) {
        YYCURSOR++;
    }

    while (YYCURSOR < YYLIMIT) {
        switch (*YYCURSOR++) {
            case '"':
                break;
            case '$':
                if (IS_LABEL_START(*YYCURSOR) || *YYCURSOR == '{') {
                    break;
                }
                continue;
            case '{':
                if (*YYCURSOR == '$') {
                    break;
                }
                continue;
            case '\\':
                if (YYCURSOR < YYLIMIT) {
                    YYCURSOR++;
                }
                /* fall through */
            default:
                continue;
        }

        YYCURSOR--;
        break;
    }

double_quotes_scan_done:
    yyleng = YYCURSOR - SCNG(yy_text);

    zend_scan_escape_string(zendlval, yytext, yyleng, '"' TSRMLS_CC);
    return T_ENCAPSED_AND_WHITESPACE;
}
View Code

 

最终返回T_ENCAPSED_AND_WHITESPACE 标签,并将 字符串放入zendval中

2)再次循环执行lex_scan , 因为全局变量已保存了前面扫描过的字符了,所以从那之后进行扫描 ,

  如果 发现 $, 如果$后面的字符在 a-z , A-Z , _ , \x7f-\xff 的范围内,或者$后面是一个左括号 { , 那么说明$后面的将是一个变量,跳转到ST_DOUBLE_QUOTES,进入

  

<ST_IN_SCRIPTING,ST_DOUBLE_QUOTES,ST_HEREDOC,ST_BACKQUOTE,ST_VAR_OFFSET>"$"{LABEL} {
  zend_copy_value(zendlval, (yytext+1), (yyleng-1));
  zendlval->type = IS_STRING;
  return T_VARIABLE;
}

  详见这个例子

<?php
$ABC='ABC';
$name="taek$ABC#";
echo $name;

 

结果是 taekABC#

PHP记法分析把 $ABC当作一个变量,而没有把$ABC#当作变量,因为ABC#不符合 LABEL [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]* 这个规则

 

2)发现{,如果紧接其后是一个$,则

  

<ST_DOUBLE_QUOTES,ST_BACKQUOTE,ST_HEREDOC>"{$" {
    zendlval->value.lval = (long) '{';
    yy_push_state(ST_IN_SCRIPTING TSRMLS_CC);
    yyless(1);
    return T_CURLY_OPEN;
}

 

#define IS_LABEL_START(c) (((c) >= 'a' && (c) <= 'z') || ((c) >= 'A' && (c) <= 'Z') || (c) == '_' || (c) >= 0x7F)

 

o
粉丝 0
博文 500
码字总数 0
作品 0
私信 提问
加载中
请先登录后再评论。
PHP脚本的执行

目前编程语言可以分为两大类: 第一类是像C/C++, .NET, Java之类的编译型语言, 它们的共性是: 运行之前必须对源代码进行编译,然后运行编译后的目标文件。 第二类比如:PHP, Javascript, ...

abulo
2014/05/14
18
0
PHP脚本的生命周期

PHP四层架构 开始之前,先看看PHP的核心架构,如图: 我们自顶向下的来看这架构: 首先是Application,就是我们的上层应用——平时写的PHP程序,可以是web应用或者php脚本; 接着是SAPI(Ser...

vinci321
2019/04/04
5
0
全方位深度剖析PHP7底层源码

第1章 课程介绍 本章主要介绍课程要讲的知识点,以及课程要求等。 1-1 课程整体介绍(了解课程整体内容,方能更好学习) 试看 第2章 PHP7的新特性 本章主要介绍PHP7的新特性,做基准测试,与...

花开月圆
2019/03/21
13
0
自己实现一个js框架(二)

jQuery采用的sizzle引擎,实现了一个解释器,也就是DSL语言。 编译原理知识学习: 第一步:词法分析,先理解一个一个的词(words)和关键词,再是理解一句话 第二步:解析成一棵树 第三步:语义...

纽约的老郑
2016/01/06
24
0
全方位深度剖析PHP7底层源码

高级工程师的核心能力-阅读源码。本课程将带你深入剖析PHP7源码,让你全面掌握PHP7源码背后的原理,内核机制、核心技术点、PHP扩展及架构设计思想等,提高你的源码阅读和分析能力,提升你的编...

学海无涯任我行
2019/06/17
9
0

没有更多内容

加载失败,请刷新页面

加载更多

LINUX_VERSION_CODE与KERNEL_VERSION

由于Linux版本的在不断更新,当设备驱动去兼容不同版本的内核时,需要知道当前使用的内核源码版本,以此来调用对应版本的内核API,这两个宏定义在文件 /usr/include/linux/version.h#defin...

osc_5g68egoj
9分钟前
11
0
JVM09-类加载过程

这一篇我们来学习一下JVM中的类加载过程。说到类的加载过程,我们需要先了解一下JVM中类的生命周期。在JVM中类的生命周期有七个阶段。分别是: 加载(Loading):加载是通过类加载器从不同的...

osc_zai0dt9q
10分钟前
9
0
###豪豪豪豪######2020 推荐系统技术演进趋势了解

读知乎文章《推荐系统技术演进趋势:从召回到排序再到重排》笔记: 《推荐系统技术演进趋势:从召回到排序再到重排》这篇文章主要说了下最近两年,推荐系统技术的一些比较明显的技术发展趋势...

osc_lhmderwy
11分钟前
9
0
SpringBoot入门实现RESTFUL API以及用Postman测试

Model @Data@Builderpublic class Article { private Long id; private String author; private String title; private String content; private Date createTime;}......

osc_7ludm6s2
12分钟前
4
0
Leetcode 83 删除排序链表中的重复元素-链表双指针

维护两个指针,第一个指针指向链表没有重复元素的最后一个位置,第二个指针向后扫描,直到末尾。严格来说,在C++中需要手动释放内存。但在算法题或者Java中不需要这么做。 class Solution {...

osc_n1x6m26g
14分钟前
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部