乐趣区

PHP源码学习20190320-PHP词法分析

【PHP 源码学习】2019-03-20 PHP 词法分析

baiyan

全部视频:https://segmentfault.com/a/11…

原视频地址:http://replay.xesv5.com/ll/24…

基本概念

  • 在 PHP7 中,当一个请求到来时,先加载对应的 PHP 代码,后进行词法分析和语法分析并生成抽象语法树(AST),然后进行深度优先遍历并生成 opcodes,在 zend 虚拟机中执行这些 opcode 并返回执行结果。在 PHP 中,使用的词法分析器是 Re2c,语法分析器是 Bison。
  • 词法分析:词法分析阶段是编译过程的第一个阶段。这个阶段的任务是从左到右一个字符一个字符地读入源程序,即对构成源程序的字符流进行扫描然后根据构词规则识别单词(也称单词符号或符号,即 token)。词法分析程序实现这个任务。词法分析程序可以使用 Re2c、lex 等工具自动生成。
  • 语法分析:语法分析是编译过程的一个逻辑阶段。语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等。语法分析程序判断源程序在结构上是否正确。
  • 其实进行词法分析和语法分析并生成某种数据结构的过程,就是一个解码的过程。

之所以需要做这种从字符串到数据结构(AST)的转换,是因为编译器是无法直接操作“1+2”这样的字符串的。实际上,代码的本质根本就不是字符串,它本来就是一个具有复杂拓扑的数据结构,就像电路一样。“1+2”这个字符串只是对这种数据结构的一种“编码”,就像 ZIP 或者 JPEG 只是对它们压缩的数据的编码一样。
这种编码可以方便你把代码存到磁盘上,方便你用文本编辑器来修改它们(对人友好,方便人们编写代码,但是对编译器不友好),然而你必须知道,文本并不是代码本身。所以从磁盘读取了文本之后,你必须先“解码”,才能方便地操作代码的数据结构。比如,如果上面的 Java 代码生成的 AST 节点叫 node,你就可以用 node.operator 来访问加号 ADD,用 node.left 来访问 1,node.right 来访问 2。这是很方便的。对于程序语言,这种解码的动作就叫做 parsing,用于解码的那段代码就叫做 parser。

  • 关于语法分析与词法分析的具体概念解释,这篇文章写得较好:对 Parser 的误解
  • 我们先利用 PHP 内置函数 token_get_all()来取出一段 PHP 代码的 token:
<?php
$lan = '<?php $a = 1; echo $a';
$tokens = token_get_all($lan);
foreach ($tokens as $token) {if (is_array($token)) {echo "Line {$token[2]}:", token_name($token[0]), "('{$token[1]}')", PHP_EOL;
    }
}
  • 打印结果为:
Line 1: T_OPEN_TAG ('<?php')
Line 1: T_VARIABLE ('$a')
Line 1: T_WHITESPACE (' ')
Line 1: T_WHITESPACE (' ')
Line 1: T_LNUMBER ('1')
Line 1: T_WHITESPACE (' ')
Line 1: T_ECHO ('echo')
Line 1: T_WHITESPACE (' ')
Line 1: T_VARIABLE ('$a')
  • 观察以上结果,可以看到取出来的 token。

如何取出 token

  • 那么让我们你自己去设计一个算法,从一个字符串中识别并取出 token,应该怎么做?

    • 使用两个指针,一个标记开始位置,一个往后挪,然后回溯。(较麻烦)
    • 使用正则表达式进行匹配
  • 当用较简单的字符串匹配正则表达式的时候,可以用人眼很容易地看出来。但是如果用很复杂的字符串(成千上万行代码)去匹配一个正则,是相当麻烦并且非常慢的,编译原理中提出了这样一个概念用以解决这个问题:有穷状态机
  • 有穷状态机:必须有一个起始状态,用一个箭头加圆圈表示;也得有一个结束,用两个圆圈表示。如果满足某个条件,就会从一个状态跃迁到另一个状态,也用箭头来表示。

例:观察下面这个正则表达式:

(a|b)*abb
  • 根据这个正则表达式,我们可以画出它的有穷状态机:

- 对于 a,只能到状态 0 或者 1,不能到达结束的 3,所以不匹配
- 对于 abb,第一个 a 可以使状态 0 跃迁到 1,第二个 b 可以从 1 跃迁到 2,最后一个 b 结束,所以匹配
- 对于 aabb,第一个 a 可以选择从 0 跃迁到 0,第二个从 0 跃迁到 1,后面两个 b 同上,匹配
- 对于 cabb,第一个 c 就无法满足,不匹配
  • 这里有个问题,输入第一个 a 的时候,可以从 0 跃迁到自己,也可以从 0 跃迁到 1,所以这种状态机就叫 不确定有穷状态机(NFA)
  • NFA 是有缺陷的,比如 aabb,有可能一直从 0 跃迁到 0,共重复了 4 次这样的操作,也没有到达最终的结束状态 3。这就会导致本应该符合匹配要求的字符串,在不确定有穷状态机中,错误地被判定为不符合匹配要求。解决此问题的办法就是将不确定有穷状态机转化为确定有穷状态机(DFA)。

  • 这样一来,一个确定的输入就对应着一个确定的输出(假设如给一个 a,一定跃迁到 1;给一个 b,一定跃迁回 0),不存在歧义问题。
  • 但是,将一个 NFA 转化成 DFA 是相当复杂的,所以有工具已经为我们做好了这个事情:Re2c。你只需要输入一个正则表达式,就能够为你生成一个确定有穷状态机(DFA),在 Re2c 工具中以 C /C++ 代码体现,详情见:re2c 中文手册
退出移动版