通常来说,PHP 从源码到产生输出,PHP 解释器需要经历四个步骤:
- 词法分析
- 语法解析
- 编译
- 解释
在此,笔者将为读者展示每一步的具体操作以及输出结果。
⒈ 词法分析(Lexing)
词法分析又称为符号化。
在此过程中,PHP 源代码被转换成了符号序列(sequence of tokens)。每一个符号(token)即为与之相匹配的值(PHP 源码中的各种字符串)的标识符。
以下 DEMO 将演示词法分析过程的输出,此操作需要开启 PHP 的 tokenizer 扩展。
<?php
$code = <<<'code'
<?php
$a = 1;
echo 60 * 60 * 24;
echo __FILE__;
if (! $a) {
echo true;
}
code;
$tokens = token_get_all($code);
foreach ($tokens as $token) {
if (is_array($token)) {
echo 'Line ' . $token[2] . ':' . $token[0] . '(' . $token[1] . ')' . PHP_EOL;
} else {
echo var_export($token, true) . PHP_EOL;
}
}
以上代码的输出如下:
Line 1:382(<?php
)
Line 2:312($a)
Line 2:385( )
'='
Line 2:385( )
Line 2:309(1)
';'
Line 2:385(
)
Line 3:324(echo)
Line 3:385( )
Line 3:309(60)
Line 3:385( )
'*'
Line 3:385( )
Line 3:309(60)
Line 3:385( )
'*'
Line 3:385( )
Line 3:309(24)
';'
Line 3:385(
)
Line 4:324(echo)
Line 4:385( )
Line 4:374(__FILE__)
';'
Line 4:385(
)
Line 6:322(if)
Line 6:385( )
'('
'!'
Line 6:385( )
Line 6:312($a)
')'
Line 6:385( )
'{'
Line 6:385(
)
Line 7:324(echo)
Line 7:385( )
Line 7:311(true)
';'
Line 7:385(
)
'}'
此处有两点需要注意:
① 并不是所有的 PHP 源代码都被词法分析器转换成了 token,像 ‘=’、‘;’、‘*’、
‘{’、‘}’ …… 这些本身就被认为是 token。
② 词法分析器不仅将源码转换成了 token,同时还记录了词位(token 所匹配的值)和行号(方便 stack trace)。
⒉ 语法解析
PHP 解析器使用与上下文无关的 LALR(look ahead 前瞻,left-to-right 从左到右) 语法。前瞻,意味着解析器为了解决在解析 token 的过程中可能遇到的歧义而提前看 n 个 token。从左到右,即解析器按照从左到右的顺序解析 token。
解析器将上一步的输出作为输入。在真正开始解析之前,解析器首先会通过尝试将输入与预先定义的语法规则进行匹配来对输入进行校验,这样保证了语言结构的有效性。在校验完成之后,解析器会生成抽象语法树(abstract syntax tree AST),AST 在下一步编译的时候会用到。
通过 php-ast 扩展可以查看解析器的输出。
ast\Node::__set_state(array(
'kind' => 132,
'flags' => 0,
'lineno' => 1,
'children' =>
array (
0 =>
ast\Node::__set_state(array(
'kind' => 517,
'flags' => 0,
'lineno' => 2,
'children' =>
array (
'var' =>
ast\Node::__set_state(array(
'kind' => 256,
'flags' => 0,
'lineno' => 2,
'children' =>
array (
'name' => 'a',
),
)),
'expr' => 1,
),
)),
1 =>
ast\Node::__set_state(array(
'kind' => 283,
'flags' => 0,
'lineno' => 3,
'children' =>
array (
'expr' =>
ast\Node::__set_state(array(
'kind' => 520,
'flags' => 3,
'lineno' => 3,
'children' =>
array (
'left' =>
ast\Node::__set_state(array(
'kind' => 520,
'flags' => 3,
'lineno' => 3,
'children' =>
array (
'left' => 60,
'right' => 60,
),
)),
'right' => 24,
),
)),
),
)),
2 =>
ast\Node::__set_state(array(
'kind' => 283,
'flags' => 0,
'lineno' => 4,
'children' =>
array (
'expr' =>
ast\Node::__set_state(array(
'kind' => 0,
'flags' => 374,
'lineno' => 4,
'children' =>
array (
),
)),
),
)),
3 =>
ast\Node::__set_state(array(
'kind' => 133,
'flags' => 0,
'lineno' => 6,
'children' =>
array (
0 =>
ast\Node::__set_state(array(
'kind' => 534,
'flags' => 0,
'lineno' => 6,
'children' =>
array (
'cond' =>
ast\Node::__set_state(array(
'kind' => 270,
'flags' => 14,
'lineno' => 6,
'children' =>
array (
'expr' =>
ast\Node::__set_state(array(
'kind' => 256,
'flags' => 0,
'lineno' => 6,
'children' =>
array (
'name' => 'a',
),
)),
),
)),
'stmts' =>
ast\Node::__set_state(array(
'kind' => 132,
'flags' => 0,
'lineno' => 6,
'children' =>
array (
0 =>
ast\Node::__set_state(array(
'kind' => 283,
'flags' => 0,
'lineno' => 7,
'children' =>
array (
'expr' =>
ast\Node::__set_state(array(
'kind' => 257,
'flags' => 0,
'lineno' => 7,
'children' =>
array (
'name' =>
ast\Node::__set_state(array(
'kind' => 2048,
'flags' => 1,
'lineno' => 7,
'children' =>
array (
'name' => 'true',
),
)),
),
)),
),
)),
),
)),
),
)),
),
)),
),
))
以上输出中,每一个 ast\Node 都包含以下四个属性:
- kind 常量表示的节点类型,实际为整数
- flags 包含节点的特定标识,大多数情况下为 0
- lineno 源码中的行号
- children 子节点,可以是 ast\Node 对象,也可以是纯值
更多关于 Node 节点的信息,可以参阅这里
⒊ 编译
编译时会将上一步生成的 AST 作为输入,通过递归遍历 AST 生成 opcodes。此外,编译的过程中还会适当的做一些优化,包括一些简单的函数调用(e.g. 将 strlen('abc') 转换为 int(3))以及计算常数的数学表达式(e.g. 将 60 *60 *24 转换为 int(86400))。
使用 vld 扩展可以查看编译的结果
php -dopcache.enable_cli=1 -dopcache.optimization_level=0 -dvld.active=1 -dvld.execute=0 test.php
输出为
在编译过程中,常量 __FILE__ 被转换成了实际的文件路径;数学表达式 60 *60 *24 被转换成了 86400。
⒋ 解释
这一步将解释上一步生成的 opcodes,opcodes 将在 Zend 虚拟机上运行,产生 PHP 源代码本来想要产生的输出。