词法分析和语法分析

广义而言,语言是一套采用共同符号、表达方式与处理规则。就编程语言而言,编程语言也是特定规则的符号,用来传达特定的信息,自然语言是人与人之间沟通的渠道,而编程语言则是机器之间,人与机器之间的沟通渠道。人有非常复杂的语言能力,语言本身也在不断的进化,人之间能够理解复杂的语言规则,而计算机并没有这么复杂的系统,它们只能接受指令执行操作,编程语言则是机器和人(准确说是程序员)之间的桥梁,编程语言的作用就是将语言的特定符号和处理规则进行翻译,由编程语言来处理这些规则,

目前有非常多的编程语言,不管是静态语言还是动态语言都有固定的工作需要做:将代码编译为目标指令,而编译过程就是根据语言的语法规则来进行翻译,我们可以选择手动对代码进行解析,但这是一个非常枯燥而容易出错的工作,尤其是对于一个完备的编程语言而言,由此就出现了像lex/yacc这类的编译器生成器。

编程语言的编译器(compiler)或解释器(interpreter)一般包括两大部分:

  • 读取源程序,并处理语言结构。
  • 处理语言结构并生成目标程序。
    Lex和Yacc可以解决第一个问题。第一个部分也可以分为两个部分:

  • 将代码切分为一个个的标记(token)。

  • 处理程序的层级结构(hierarchical structure)。
    很多编程语言都使用lex/yacc或他们的变体(flex/bison)来作为语言的词法语法分析生成器,比如PHP、Ruby、Python以及MySQL的SQL语言实现。

Lex和Yacc是Unix下的两个文本处理工具, 主要用于编写编译器, 也可以做其他用途。

  • Lex(词法分析生成器:A Lexical Analyzer Generator)。
  • Yacc(Yet Another Compiler-Compiler)

Lex/Flex

Lex读取词法规则文件,生成词法分析器。目前通常使用Flex以及Bison来完成同样的工作, Flex和lex之间并不兼容,Bison则是兼容Yacc的实现。

词法规则文件一般以.l作为扩展名,flex文件由三个部分组成,三部分之间用%%分割:

  1. 定义段
  2. %%
  3. 规则段
  4. %%
  5. 用户代码段

例如以下一个用于统计文件字符、词以及行数的例子:

  1. %option noyywrap
  2. %{
  3. int chars = 0;
  4. int words = 0;
  5. int lines = 0;
  6. %}
  7.  
  8. %%
  9. [a-zA-Z]+ { words++; chars += strlen(yytext); }
  10. \n { chars++; lines++; }
  11. . { chars++; }
  12. %%
  13.  
  14. main(int argc, char **argv)
  15. {
  16. if(argc > 1) {
  17. if(!(yyin = fopen(argv[1], "r"))) {
  18. perror(argv[1]);
  19. return (1);
  20. }
  21. yylex();
  22. printf("%8d%8d%8d\n", lines, words, chars);
  23. }
  24. }

该解释器读取文件内容, 根据规则段定义的规则进行处理, 规则后面大括号中包含的是动作, 也就是匹配到该规则程序执行的动作,这个例子中的匹配动作时记录下文件的字符,词以及行数信息并打印出来。其中的规则使用正则表达式描述。

回到PHP的实现,PHP以前使用的是flex,后来PHP的词法解析改为使用re2c,$PHP_SRC/Zend/zend_language_scanner.l 文件是re2c的规则文件, 所以如果修改该规则文件需要安装re2c才能重新编译。

Yacc/Bison

PHP在后续的版本中可能会使用Lemon作为语法分析器Lemon是SQLite作者为SQLite中SQL所编写的词法分析器。 Lemon具有线程安全以及可重入等特点,也能提供更直观的错误提示信息。

Bison和Flex类似,也是使用%%作为分界不过Bison接受的是标记(token)序列,根据定义的语法规则,来执行一些动作,Bison使用巴科斯范式(BNF)来描述语法。

下面以php中echo语句的编译为例:echo可以接受多个参数,这几个参数之间可以使用逗号分隔,在PHP的语法规则如下:

  1. echo_expr_list:
  2. echo_expr_list ',' expr { zend_do_echo(&$3 TSRMLS_CC); }
  3. | expr { zend_do_echo(&$1 TSRMLS_CC); }
  4. ;

其中echo_expr_list规则为一个递归规则,这样就允许接受多个表达式作为参数。在上例中当匹配到echo时会执行zend_do_echo函数,函数中的参数可能看起来比较奇怪, 其中的$3 表示前面规则的第三个定义,也就是expr这个表达式的值,zend_do_echo函数则根据表达式的信息编译opcode,其他的语法规则也类似。这和C语言或者Java的编译器类似,不过GCC等编译器时将代码编译为机器码,Java编译器将代码编译为字节码。

更多关于lex/yacc的内容请参考Yacc 与Lex 快速入门

下面将介绍PHP中的opcode。

原文: http://www.php-internals.com/book?p=chapt02/02-03-01-lex-and-yacc