总则
词法分析器的主要功能是切分输入的文本串。将输入串分割为各种类型的记号。这了实现分词,有两种主要的作法,一种是有限状态机,一种是正则表达式匹配。本项目选用PHP做为开发语言,自然可以利用PHP中方便易用的正则表达式功能进行分词。
具体实施
仅仅使用php的正则表达式,还不足以构造一个好的词法分析器。因此在实施时还采取了如下一些措施
1.将词法规则表与词法分析引擎分离,这样做的好处是,当要处理新类型的源代码时,不用变动词法分析引擎,而只需要修改词法规则表。
2.为提高分词的效率,采用首字母分类hash法,也就是说对一个给定的文本串,取其首字母,根据首字母为关键字建立的hash表,可以极大地缩小需要进行匹配的词法规则项。也就是先用首字母查找再用正则表达式匹配,这样可以极大地减少需要进行正则表达式匹配的次数。
3.为了能够处理中文文本,要求源码采用utf8编码,并且对词法规则与词法分析引擎做特别处理,比如求字符串长度时,用mb_strlen($input,“utf-8”)代替strlen,
取子串时,用mb_substr代替substr等。
词法规则表
下面是一个词法规则表示例,注意,词法规则表是可以根据需要随时扩展的,并且非常方便扩展,这也是本项目的一个最重要的设计原则。
<?php
/*!
* ADOS 语言的词法规则
* 45022300@qq.com
* Version 0.9.0
*
* Copyright 2016, Zhu Hui
* Released under the MIT license
*/
return [
['/^\/\*/','_lcom' ,'/'],
['/^\*\//','_rcom' ,'*'],
['/(^#.*?\n)/s','_com' ,'#'],
['/^\"(.*?)\"/','_cons' ,'"'],
['/^\(/','_lp' ,'('],
['/^\)/','_rp' ,')'],
['/^\[/','_lb' ,'['],
['/^\]/','_rb' ,']'],
['/^\{/','_lcb' ,'{'],
['/^\}/','_rcb' ,'}'],
['/^;/','_semi' ,';'],
['/^,/','_comma' ,','],
['/^==/','_bieq' ,'='],
['/^!=/','_uneq' ,'!'],
['/^\>=/','_greq' ,'>'],
['/^小于等于/u','_leeq' ,'小'],
['/^小于或等于/u','_leeq' ,'小'],
['/^\<\?ados\b/','_null','<'],
['/^\<\?/','_null' ,'<'],
['/^\<=/','_leeq' ,'<'],
['/^赋值为/u','_equa' ,'赋'],
['/^变成/u','_equa' ,'变'],
['/^=/','_equa' ,'='],
['/^\>/','_grea' ,'>'],
['/^\</','_less' ,'<'],
['/^拼接/u','_union' ,'拼'],
['/^\+\+/','_union' ,'+'],
['/^增加/u','_inc' ,'增'],
['/^减少/u','_dec' ,'减'],
['/^\+=/','_inc' ,'+'],
['/^-=/','_dec' ,'-'],
['/^\+/','_add' ,'+'],
['/^-/','_sub' ,'-'],
['/^\*/','_mul' ,'*'],
['/^\//','_div' ,'/'],
['/^%/','_mod' ,'$'],
['/^@/','_at' ,'@'],
['/^&&/','_and' ,'&'],
['/^\|\|/','_or' ,'|'],
['/^!/','_not' ,'!'],
['/^[0-9]+([.]{1}[0-9]+){0,1}/','_num',''],
['/^\./','_dot' ,'.'],
['/^让\b/u','_null' ,'让'],
['/^try\b/','_try' ,'t'],
['/^执行\b/u','_try' ,'执'],
['/^wait\b/','_wait' ,'w'],
['/^等待\b/u','_wait' ,'等'],
['/^如果\b/u','_if' ,'如'],
['/^if\b/','_if' ,'i'],
['/^则\b/u','_null' ,'则'],
['/^则重复执行\b/u','_null','则'],
['/^时则重复执行\b/u','_null','时'],
['/^否则\b/u','_else' ,'否'],
['/^else\b/','_else' ,'e'],
['/^goto\b/','_goto' ,'g'],
['/^转到\b/u','_goto' ,'转'],
['/^for\b/','_for' ,'f'],
['/^当\b/u','_while' ,'当'],
['/^while\b/','_while' ,'w'],
['/^var\b/','_var' ,'v'],
['/^function\b/','_func','f'],
['/^return\b/','_retn' ,'r'],
['/^class\b/','_class' ,'c'],
['/^this\b/','_this' ,'t'],
['/^public\b/','_public','p'],
['/^private\b/','_private','p'],
['/^new\b/','_new' ,'n'],
['/^from\b/','_from' ,'f'],
['/^输出\b/u','_print' ,'输'],
['/^print\b/','_print' ,'p'],
['/^[\x{4e00}-\x{9fa5}A-Za-z_][\x{4e00}-\x{9fa5}A-Za-z0-9_]*:/u','_lbel',''],
['/^[\x{4e00}-\x{9fa5}A-Za-z_][\x{4e00}-\x{9fa5}A-Za-z0-9_]*\b/u','_iden',''],
['/^\s*/','_null','']
];
稍微留意一下,可以看出词法规则表其实就是php中的数组,数组的每一项又是一个三元组。
下面取其中的几个做例子来解释
例1:
['/^\/\*/','_lcom' ,'/'],
这是一个三元组,第一部分是一个正则表达式的模式串,第二部分是记号名(tokenName),第三部分是这个记号的首字母。
对正则表达式比较熟悉的人可以看出来,这个模式会匹配注释符号’/*’,如果匹配了,就把这个token叫做 _lcom ,表示这是一个注释块的左侧开头部分。
例2:
['/^[0-9]+([.]{1}[0-9]+){0,1}/','_num',''],
这个三元组的第一部是一个模式串,与这个模式串匹配的一定是一个数字串,比如123,可以与这个模式匹配,但a12就不能与这个模式匹配。如果匹配上了,表明切分出了一个记号,这个记号是一个数字串,它的tokenName就是 _num。有意思的一点是由于数字串的首字母不固定,所以这个三元组的第三部分是一个空串,表明其没有固定的首字母,在建立hash表时,这些没有固定首字母的词法规则单列一类。
例3:
['/^如果\b/u','_if' ,'如'],
这个三无组的第一部分是一个模式串,它与中文单词’如果’匹配,也就是说源代码中出现‘如果’地方,将被识别为一个关键词,它的tokenName称之为‘_if ’。
留意一下,该规则接下的一条规则是
['/^if\b/','_if' ,'i'],
这就说明不论是中文的‘如果’还是英文的‘if’,经过词法分析器处理之后,都变成tokenName为’_if’的词法记号。
所有的词法规则都按上述原则进行组织。
词法分析引擎
词法分析引擎实现代码如下:
<?php
/*!
* ADOS 词法分析器
* 45022300@qq.com
* Version 0.9.0
*
* Copyright 2016, Zhu Hui
* Released under the MIT license
*/
namespace Ados;
//根据源语言的类型加载词法规则表
$lexi_rules = require_once 'lexi_rule/'.C('SrcLang').'.php';
class ScriptLexer
{
//终结符数组
public $terminalArray;
//规则前缀索引表
private $rulePrefixTable;
public function __construct()
{
$this->buildTerminalArray();
$this->rulePrefixTable=$this->buildRulePrefixTable();
}
//将输入串分解为记号数组
public function split($input)
{
$tokenList=[];
$beginTime=time();
//行号与列号从1开始计数
$lineNum = 1;
$colNum = 1;
while(mb_strlen($input,"utf-8")>0){
$pos=0;
//如果使用substr,中文可能会出现乱码,因此使用对中文处理不会乱码的mb_substr
$firstChar=mb_substr($input, 0,1,"utf-8");
if(array_key_exists($firstChar, $this->rulePrefixTable)){
foreach ($this->rulePrefixTable[$firstChar] as $rule) {
if (preg_match($rule[0], $input, $matches, PREG_OFFSET_CAPTURE)){
if($rule[1]=='_null'){
//检查空白串中包含的换行符的数目
str_replace("\n", '', $matches[0][0],$cnt);
$lineNum+=$cnt;
//确定列号
if($cnt>0){
$colNum = 1;
}
$colPos = strrpos($matches[0][0], "\n");
if ($colPos === false) { // 注意: 三个等号
$colNum+=mb_strlen($matches[0][0],"utf-8");
}else{
$colNum+=mb_strlen($matches[0][0],"utf-8")-$colPos-1;
}
}else{
if($rule[1]=='_com'){
//行注释中包含换行符
$lineNum+=1;
$colNum = 1;
}else{
$tokenValue=$matches[0][0];
if($rule[1]=='_cons'){
$tokenValue=mb_substr($tokenValue,1,null,"utf-8");
$tokenValue=mb_substr($tokenValue,0,mb_strlen($tokenValue,"utf-8")-1,"utf-8");
}
//插入有效的记号到数组,记号格式为 [记号名,记号值,记号在原文中的行号,记号在原文中的列号]
$tokenList[]=[$rule[1],$tokenValue,$lineNum,$colNum];
$colNum+=mb_strlen($matches[0][0],"utf-8");
}
}
$pos=mb_strlen($matches[0][0],"utf-8");
$input=mb_substr($input,$pos,null,"utf-8");
break;
}
}
}
if(!$pos){
foreach ($this->rulePrefixTable['other'] as $rule) {
if (preg_match($rule[0], $input, $matches, PREG_OFFSET_CAPTURE)){
if($rule[1]=='_null'){
//检查空白串中包含的换行符的数目
str_replace("\n", '', $matches[0][0],$cnt);
$lineNum+=$cnt;
//确定列号
if($cnt>0){
$colNum = 1;
}
$colPos = strrpos($matches[0][0], "\n");
if ($colPos === false) { // 注意: 三个等号
$colNum+=mb_strlen($matches[0][0],"utf-8");
}else{
$colNum+=mb_strlen($matches[0][0],"utf-8")-$colPos-1;
}
}else{
if($rule[1]=='_com'){
//行注释中包含换行符
$lineNum+=1;
$colNum = 1;
}else{
$tokenValue=$matches[0][0];
if($rule[1]=='_cons'){
$tokenValue=mb_substr($tokenValue,1,null,"utf-8");
$tokenValue=mb_substr($tokenValue,0,mb_strlen($tokenValue,"utf-8")-1,"utf-8");
}
//插入有效的记号到数组,记号格式为 [记号名,记号值,记号在原文中的行号,记号在原文中的列号]
$tokenList[]=[$rule[1],$tokenValue,$lineNum,$colNum];
$colNum+=mb_strlen($matches[0][0],"utf-8");
}
}
$pos=mb_strlen($matches[0][0],"utf-8");
$input=mb_substr($input,$pos,null,"utf-8");
break;
}
}
}
//如果没有一个正则表达式能匹配,则说明有词法错误
if(!$pos){
$srcText="'".mb_substr($input,$pos,2,"utf-8")."'";
die (I('lexi error').$lineNum.",".I('unexpected').$srcText."\n");
}
}
$usedTime= time()-$beginTime;
//echo I('used time :'),$usedTime,"\n<br>";
return $tokenList ;
}
//分行显示记号数组
public function printTokenList($tokenList){
$i=0;
foreach ($tokenList as $token ) {
echo $i,"->[",$token[2],',',$token[3],"] ", $token[0],' : ',$token[1],"\n";
$i+=1;
}
}
//处理记号列表,除lcom与rcom的记号都置为other
public function commentTokenList($tokenList){
$resultList=[];
foreach ($tokenList as $token ) {
if($token[0]=='_lcom' || $token[0]=='_rcom'){
$resultList[]=$token;
}else{
$resultList[]=['_other',$token[1],$token[2],$token[3]];
}
}
return $resultList;
}
//建立规则前缀索引表,以规则左部应匹配的第1个字符为键值建立哈希表
private function buildRulePrefixTable(){
global $lexi_rules;
$tbl=[];
$tbl['other']=[];
foreach ($lexi_rules as $rule) {
$key=$rule[2];
if($key==''){
$tbl['other'][]=$rule;
}else{
if(array_key_exists($key, $tbl)) {
$tbl[$key][]=$rule;
}else{
$tbl[$key]=[];
$tbl[$key][]=$rule;
}
}
}
return $tbl;
}
//建立终结符表
private function buildTerminalArray(){
global $lexi_rules;
$this->terminalArray=[];
foreach ($lexi_rules as $rule){
$this->terminalArray[]=$rule[1];
}
}
}
词法分析引擎是一个php的class。它的核心方法是split。这个方法根据上文所述的词法规则表,将输入的源文本串切分成为记号(token)数组。
词法分析实例
下面是一个很简单的源文本
让 x 变成 (10+2)*3;
输出 "x -> " 拼接 x ;
这两句话很好理解,也就是让 x这个变量的值变成 (10+2)*3, 即让x变成36嘛,然后输出 ‘x->36’。
这里先不管具体的表达式计算,那是后话,要在语法分析阶段去做。现在咱们只看一下词法分析的结果。
0->[1,3] _iden : x
1->[1,5] _equa : 变成
2->[1,8] _lp : (
3->[1,9] _num : 10
4->[1,11] _add : +
5->[1,12] _num : 2
6->[1,13] _rp : )
7->[1,14] _mul : *
8->[1,15] _num : 3
9->[1,16] _semi : ;
10->[2,1] _print : 输出
11->[2,4] _cons : x ->
12->[2,12] _union : 拼接
13->[2,15] _iden : x
14->[2,17] _semi : ;
对以上结果做一个简单的说明
针对上述的两行源文本做词法分析之后,得到了15个词法记号(token)
第0个token是’_iden’ (标示符),它的值是’x’,它在源文件中的起点是第1行第3列。(备注 源文件中的单词’让’是一个助词,直接被词法分析器忽略了)
第1个token是’_equa’ (等号),它的值是’变成’,它在源文件中的起点是第1行第5列
第2个token是’_lp’ (左小括号),它的值是’(’,它在源文件中的起点是第1行第8列
第3个token是’_num’ (数字串),它的值是’10’,它在源文件中的起点是第1行第9列
第4个token是’_add’ (加号),它的值是’+’,它在源文件中的起点是第1行第11列
第5个token是’_num’ (数字串),它的值是’2’,它在源文件中的起点是第1行第12列
第6个token是’_rp’ (右小括呈),它的值是’)’,它在源文件中的起点是第1行第12列
剩下的结果文本行感兴趣的朋友可以自行分析。
至此,一个简单易用的词法分析就成功创建了。