脚本编译器的目标是将源脚本文件变成可执行文件。
比如下面这一段源码
#以下是测试脚本
让 x 变成 (10+2)/50+2/5+100;
输出 "x -> " 拼接 x ;
如果(x 小于或等于 100)
{
输出 "x 是个很小的数";
}
否则
{
输出 "x 是个较大的数";
}
让 x 变成 1;
当(x 小于等于 10) 时则重复执行
{
输出 ("x -> " 拼接 x);
让 x 变成 x+1; #非专业程序员对这一句很困惑
}
输出 "x -> " 拼接 x ;
经过编译后变成了如下可执行的代码:
L0: goto L1;
L1: $env = array_pop($envStack);
L2: $env['x']=(10+2)/50+2/5+100;
L3: echo "x -> ".$env['x'],"\n";
L4: if (!($env['x']<=100)) goto L7;
L5: echo "x 是个很小的数","\n";
L6: goto L8;
L7: echo "x 是个较大的数","\n";
L8: $env['x']=1;
L9: if (!($env['x']<=10)) goto L13;
L10: echo ("x -> ".$env['x']),"\n";
L11: $env['x']=$env['x']+1;
L12: goto L9;
L13: echo "x -> ".$env['x'],"\n";
L14: goto L15;
L15: $this->procState='finished';
L16: ;
可能有的人会好奇,以上是可执行的代码吗?
是的,以上的内容就是可以执行的php代码。
但是又和平常我们见到的php代码不太一样,最明显的特点是,以上代码的每一行都有标号,而且有非常多的goto语句。有的php的开发人员可能很少用到goto语句,但实际上所有的程序结构最终都可以归结为条件goto结构。
只要有了条件跳转,分支结构,循环结构都可以用条件跳转来实现。
有的人可能会问,为什么要将源脚本编译成可执行的php脚本呢?不能编译成机器代码吗?
回答是,只要设计出相应的编译规则,源脚本就可以编译成任何你想要的目标语言,比如机器语言,汇编语言,C语言,php语言,python语言等等。
在这里我将源脚本编译成php语言后,就可以让php执行得到的编译结果。
我的目标并不是追求执行的高效,而是追求开发的高效,所以编译成php代码就可以了。实际上如果有需要,我也可以将源码编译成C语言,然后再调用C语言的编译器编译成机器代码。
为了完成从源码到目标代码的转换,我们一般不会一次到位,而是要分成几步走。大致说来,可以分成三大步
- 词法分析,将源文本切分成一个一个的记号串(token)
- 语法分析,将输入的记号(token)数组归约为各级语法单元
- 生成目标代码,根据语法分析的结果,将记号串翻译成目标语言的指令块
需要注意的是,本项目采用边进行语法分析,边进行目标代码的生成。语法分析的归约过程完成了,目标代码的翻译(编译)也就完成了。这种操作方式可以称之为语法制导翻译。
插一句,上面提到将源码编译成可执行的php代码,不需要像C编译器那样编译完成后还要做链接工作,由于不用考虑链接,所以项目就可以很简炼。即使是为了追求执行的高效,也完全可以先编译成C代码,然后再编译成可执行文件。所谓大树底下好乘凉,C语言可是一株参天大树啊。
按照上面的总的设计原则,本项目的总体结构如下:
其中
1 : script_lexer.php 为词法分析器,引用lexi_rule目录下的词法规则文件
2: script_parser.php为语法分析器,调用syntax_rule目录下的语法规则文件
3: coder目录下存放各种编码器,编码器被语法分析器在语法分析的过程中调用,用于生成目标代码,编码器与语法分析器是可组合的,要生成另一种目标语言时只要换用相应的编码器即可,其它的模块不用改动。
4: script_engine.php为目标代码的执行引擎,相当于java语言中jvm的地位,用于执行目标代码,中断,恢复执行等等。