中文脚本编译器的设计与实现(二)词法分析器

总则

词法分析器的主要功能是切分输入的文本串。将输入串分割为各种类型的记号。这了实现分词,有两种主要的作法,一种是有限状态机,一种是正则表达式匹配。本项目选用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列

剩下的结果文本行感兴趣的朋友可以自行分析。

至此,一个简单易用的词法分析就成功创建了。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值