用PHP从零开始写一个编译器(一)

1,359 阅读5分钟

用PHP写编译器,你是不是讲笑话?不是,我是认真的。

虽然,国外网站上已经有多篇关于用PHP写一个编译器的文章,但那都是属于讲解性的文档。今天我们要做的,是对RQL语言进行编译或者说解析。

那么,RQL是什么呢?RQL是Resource Query Language的简称,也就是说,它是一个资源查询语言。RQL和GraphQL是什么关系?其实没有关系,GraphQL本身就是资源查询,但它有很多附加的功能,比如,自省,数据类型。RQL就没有那么复杂,它只是查询。

为什么要写这个编译器或解析器呢?因为,如果使用RQL,就可以增强RESTful API查询,一个项目中,大量的接口,均可以通过通用的代码完成。

那么,RQL语言具体有些什么样的语法呢? 相当简单,即是函数结构。

比如,a = b , RQL 写出来就是 eq(a,b)。更具体的,可以参考:

byteferry.github.io/rql-parser/…

那么,具体的需求是什么呢?

把RQL解析成MVC框架中,SERVICE层可用的参数,从而能够通过ApiBridge完成对应函数的调用。

很简单。但是,我们约定几个规范,

必须使用设计模式。所有函数,不得超过50行,if嵌套不得超过三层。变量,下划线式命名(考虑到数据库中均用下划线,另外,变量可以与其它类型有所区分),其它(函数,类等)均以驼峰命名。最终代码,必须用Travis CI 持续集成通过,代码覆盖率95%以上。这样做的目的,是真正做一个受欢迎的开源。真正方便未来维护。绝对不允许像Tp那样,一个函数近200行,测试代码覆盖率百分之二十多。

接下来就要真的动手了。这里得使用我们在大学的《编译原理》课程中的知识。

一个编译器,有以下几个组成部分,第一是,符号表,也就是,哪些东西是要编译的。

第二,是词法,第三,是列表词法,第四,是Token, 当然,还有AST,即Abstract Syntex Tree(抽像语法树)。估计,你已经晕了。

还是上代码,先上符号表




/*
 * This file is part of the ByteFerry/Rql-Parser package.
 *
 * (c) BardoQi <67158925@qq.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */


declare(strict_types=1);

namespace ByteFerry\RqlParser\Lexer;

use ReflectionClass;
use ByteFerry\RqlParser\AstBuilder as Ast;
/**
 * Class Symbols
 *
 * @package ByteFerry\RqlParser
 */
class Symbols
{

    /**
     * The keys of Symbol List Of Lexer
     * 这里定义的是Token的类型,首先是把Token分类
     * 
     * @var array
     */
    public const  T_WORD                =  'T_WORD';
    public const  T_STRING              =  'T_STRING';
    public const  T_OPEN_PARENTHESIS    =  'T_OPEN_PARENTHESIS';   // (
    public const  T_CLOSE_PARENTHESIS   =  'T_CLOSE_PARENTHESIS';  // )
    public const  T_PLUS                =  'T_PLUS';               // +
    public const  T_COMMA               =  'T_COMMA';              // ,
    public const  T_MINUS               =  'T_MINUS';              // -
    public const  T_COLON               =  'T_COLON';              // :

    /**
     * The Symbol List Of Lexer
     * 这里是定义的是不同类型的Token所用的正则表达式。我们当然用正则表达式来获取Token,因为这样,代码最简单。
     *
     * @var array
     */
    public static $symbol_expressions = [
        'T_WORD'                => '(?<T_WORD>\w+_*?\w*?)',                 // word
        'T_OPEN_PARENTHESIS'    => '(?<T_OPEN_PARENTHESIS>\({1})',          // (
        'T_CLOSE_PARENTHESIS'   => '(?<T_CLOSE_PARENTHESIS>\){1})',         // )
        'T_STRING'              => '(?<T_STRING>".*?")|(?<T_DOT>\.{1})',    // ".*"
        'T_COLON'               => '(?<T_COLON>:{1})',                      // :
        'T_COMMA'               => '(?<T_COMMA>,{1})',                      // ,
        'T_PLUS'                => '(?<T_PLUS>\+{1})',                      // +
        'T_MINUS'               => '(?<T_MINUS>\-{1})',                     // -
    ];

    /**
     * we use the rules to ensure the rql language is correct
     * we put the rules here only for doing the  maintenance conveniently
     * 这里定义的是语法检查规则。就是不同Token后面能够跟什么样的Token
     *
     * @var array
     */
    public static $rules = [
        'T_WORD' => ['T_OPEN_PARENTHESIS','T_CLOSE_PARENTHESIS','T_COMMA','T_COLON'],
        'T_STRING' => ['T_OPEN_PARENTHESIS','T_CLOSE_PARENTHESIS','T_COMMA','T_COLON'],
        'T_OPEN_PARENTHESIS' => ['T_WORD','T_STRING','T_PLUS','T_MINUS','T_CLOSE_PARENTHESIS'],
        'T_CLOSE_PARENTHESIS' =>['T_CLOSE_PARENTHESIS','T_COMMA'],
        'T_COLON'=>['T_WORD','T_STRING'],
        'T_COMMA'=>['T_WORD','T_STRING','T_OPEN_PARENTHESIS','T_PLUS','T_MINUS'],
        'T_PLUS'=>['T_WORD'],
        'T_MINUS'=>['T_WORD']
    ];


    /**
     * list of operator aliases
     * 这里定义的是Rql关键字的别名的映射关系,
     * @var array
     */
    public static $type_alias = [
        'plus' =>'increment',
        'minus'=>'decrement',
        'cols'=>'columns',
        'only'=>'columns',
        'field'=>'columns',
        'select'=>'columns',
        'aggr'=>'aggregate',
        'mean'=>'avg',
        'nin' =>'out',
    ];

    /**
     * mapping the type to node type
     * 这里定义的是RQL关键字与语法树的节点类型的映射关系
     * @var array
     */
    public static $type_mappings = [
        'aggr' =>'N_COLUMN',
        'aggregate' =>'N_COLUMN',
        'all' =>'N_QUERY',
        'and' =>'N_LOGIC',
        'any' =>'N_QUERY',
        'arr' =>'N_ARRAY',
        'avg' =>'N_AGGREGATE',
        'between' =>'N_PREDICATE',
        'cols' =>'N_COLUMN',
        'columns' =>'N_COLUMN',
        'count' =>'N_QUERY',
        'create' =>'N_QUERY',
        'data' =>'N_DATA',
        'decrement' =>'N_QUERY',
        'delete' =>'N_QUERY',
        'distinct' =>'N_COLUMN',
        'empty' => 'N_CONSTANT',
        'eq' =>'N_PREDICATE',
        'except' =>'N_COLUMN',
        'exists' =>'N_QUERY',
        'false' => 'N_CONSTANT',
        'filter' =>'N_FILTER',
        'first' =>'N_QUERY',
        'ge' =>'N_PREDICATE',
        'gt' =>'N_PREDICATE',
        'having' =>'N_FILTER',
        'in' =>'N_PREDICATE',
        'increment' =>'N_QUERY',
        'is' => 'N_PREDICATE',
        'le' =>'N_PREDICATE',
        'like' =>'N_PREDICATE',
        'limit' =>'N_LIMIT',
        'lt' =>'N_PREDICATE',
        'max' =>'N_AGGREGATE',
        'mean' =>'N_AGGREGATE',
        'min' =>'N_AGGREGATE',
        'minus' =>'N_QUERY',
        'ne' =>'N_PREDICATE',
        'nin' =>'N_PREDICATE',
        'not' =>'N_LOGIC',
        'null'=>'N_CONSTANT',
        'one' =>'N_QUERY',
        'only' =>'N_COLUMN',
        'or' =>'N_LOGIC',
        'out' =>'N_PREDICATE',
        'plus' =>'N_QUERY',
        'search' =>'N_SEARCH',
        'select' =>'N_COLUMN',
        'sort' =>'N_SORT',
        'sum' =>'N_AGGREGATE',
        'true' => 'N_CONSTANT',
        'update' =>'N_QUERY',
        'values' =>'N_COLUMN',
    ];

    /**
     * mapping node type to class
     * 这里再把上面的节点类型,映射到对应的节点类 
     * @var array
     */
    public static $class_mapping = [
        'N_AGGREGATE' =>    Ast\AggregateNode::class,
        'N_ARRAY'=>         Ast\ArrayNode::class,
        'N_COLUMN' =>       Ast\ColumnsNode::class,
        'N_CONSTANT' =>     Ast\ConstantNode::class,
        'N_DATA' =>         Ast\DataNode::class,
        'N_FILTER' =>       Ast\FilterNode::class,
        'N_LIMIT' =>        Ast\LimitNode::class,
        'N_LOGIC' =>        Ast\LogicNode::class,
        'N_PREDICATE' =>    Ast\PredicateNode::class,
        'N_QUERY' =>        Ast\QueryNode::class,
        'N_SEARCH' =>       Ast\SearchNode::class,
        'N_SORT' =>         Ast\SortNode::class,
    ];

    /**
     * 这里定义的是RQL的操作符与实际操作符的映射  
     * @var array
     */
    public static $operators = [
        'eq' => '=',
        'ne' => '<>',
        'gt' => '>',
        'ge' => '>=',
        'lt' => '<',
        'le' => '<=',
        'is' => 'is',
        'in' => 'in',
        'out' => 'not in',
        'like' => 'like',
        'between' => 'between',
        'contains' => 'contains'
    ];

    /**
     * Query type mapping
     * 这里定义的是RQL的查询类型,是读还是写
     * @var array
     */
    public static $query_type_mapping = [
        'all'           =>  'Q_READ',
        'any'           =>  'Q_READ',
        'count'         =>  'Q_READ',
        'create'        =>  'Q_WRITE',
        'decrement'     =>  'Q_WRITE',
        'delete'        =>  'Q_WRITE',
        'exists'        =>  'Q_READ',
        'first'         =>  'Q_READ',
        'increment'     =>  'Q_WRITE',
        'one'           =>  'Q_READ',
        'update'        =>  'Q_WRITE',
    ];

    /**
     * 下面两个静态函数,用到时再讲
     * @return array
     * @throws \ReflectionException
     */
    public static function getSymbolsKey()
    {
        $reflect = new ReflectionClass(__CLASS__);
        return $reflect->getConstants();
    }

    /**
     * @return string
     */
    public static function makeExpression(){
        $expression = '/';
        $expression .= implode('|', self::$symbol_expressions);
        return $expression . '/';
    }
}

可以发现,这个类不太地道,不只是符号表,而是罗列了一堆映射关系。为什么这么做呢,目的相当简单。就是把所有类似于配置或映射的集中在这里,方便后续升级或修改。 接下来,我们要做一个解析器,来通过词法类,把输入的RQL先变成Token数组。

我们的解析器类代码如下:


declare(strict_types=1);
/*
 * This file is part of the ByteFerry/Rql-Parser package.
 *
 * (c) BardoQi <67158925@qq.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace ByteFerry\RqlParser;

use ByteFerry\RqlParser\AstBuilder\NodeVisitor;  		//引用节点访问者类
use ByteFerry\RqlParser\Lexer\Lexer;         			//用词法Lexer类
use ByteFerry\RqlParser\AstBuilder\NodeInterface;		//引用节点Interface
use ByteFerry\RqlParser\Lexer\Token;			//引用Token类
use ByteFerry\RqlParser\Lexer\ListLexer;			//引用列表词法ListLexer类
use ByteFerry\RqlParser\AstBuilder\ParamaterRegister;          //参数注册表类

/**
 * Class Parser
 *
 * @package ByteFerry\RqlParser
 */
class Parser
{
    /**
     * @var NodeInterface[]
     */
    protected $node_list = [];

    /**
     * @param \ByteFerry\RqlParser\Lexer\ListLexer $tokens
     * 这里也不复杂,关键使用了访问者模式。如果不清楚访问者模式,可能要看设计模式的书,脑补一下。
     * @return \ByteFerry\RqlParser\AstBuilder\NodeInterface[]
     */
    protected function load(ListLexer $ListLexer){
        $ListLexer->rewind();   //对拿到的ListLexer重置编移量到第一个
        /** @var Token $token */
        $token = $ListLexer->current();  // 读取当前的,也就是,读第一个。
       // 开始消费每一个token
        for(; (false !== $token); $token = $ListLexer->consume()){

            $symbol = $token->getSymbol();   //获取token的symbol
            /** @var NodeInterface $node */
            $node = NodeVisitor::visit($symbol);  //再用访问者模式,获得真正的节点对象

            $node->load($ListLexer);  // 节点再载入ListLexer
            $this->node_list[] = $node;  // 把解析成的node存入数组
        }
        return $this->node_list;   返回node 列表
    }

    /**
     * @param bool $is_segmaent
     * 这个函数相当简单,根据不同的类型,返回不同的类的实例。 
     * @return QueryInterface
     */
    protected static function getOutputObject( $is_fragmaent = false){
        if(false === $is_fragmaent){
            return Query::of();
        }
        return Fragment::of();
    }

    /**
     * @param      $string   这里是传入的RQL String
     * @param bool $is_fragmaent  这里传入的是,RQL是一个片段,还是一个完整的查询
     *  这里是一切的入口 
     *  
     * @return array
     * @throws \ByteFerry\RqlParser\Exceptions\RegexException
     */
    public static function parse($string, $is_fragmaent = false)
    {

        /** @var ListLexer $tokens */   // 首先,Lexer把RQL字符串,转换成tokens数组(列表词法类)
        $tokens = Lexer::of()->tokenise($string);

        $instance = new static();    // 创建当前类的实例
 
        ParamaterRegister::newInstance();   // 初始化参数注册表

        /** @var NodeInterface[] $node_list */
        $node_list = $instance->load($tokens);  // 通过load方法,将$tokens转换成节点列表。
        $ir_list = [] ;

        /** @var NodeInterface $node */
        foreach($node_list as $node){
            $ir_list[] = $node->build();   // 对每一个节点进行编对,存入到预编译列表中
        }

        $queries = [];
        foreach ($ir_list as $ir) {
             $query = self::getOutputObject($is_fragmaent);   //再把它转换成Query对象数组,返回。
             $queries[] = $query->from($ir);
        }
 
        return $queries;   //  到此,最复杂的RQL,耗时不到3MS,效率相当高。
    }
}

我们可以看出,这个解析器,只是抽象语法树的顶层。它只做了它清楚的事情,即:只是调用对应的类来完成。

接下来,词法类要登场了。



/*
 * This file is part of the ByteFerry/Rql-Parser package.
 *
 * (c) BardoQi <67158925@qq.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace ByteFerry\RqlParser\Lexer;

use ByteFerry\RqlParser\Abstracts\BaseObject;
use ByteFerry\RqlParser\Exceptions\ParseException;

/**
 * Class Lexer
 *
 * @package ByteFerry\RqlParser
 */
class Lexer extends BaseObject
{

    /**
     * @var array
     */
    protected $symbol_keys;

    /**
     * @var int
     */
    protected $previous_type = -1;


    /**
     * @var ListLexer | null
     */
    protected $listLexer = null;  //ListLexer(token数组容器)。

    /**
     * Lexer constructor
     *
     * we need get the array of keys of the symbols first!
     *
     */
    public function __construct(){
        $this->symbol_keys= Symbols::getSymbolsKey();  //我们调用了这个函数,把常量装进了一个数组。
    }

    /**
     * The match data is in the target key and the offset!=-1
     *
     * @param $match
     *
     * @return array
     */
    protected function getMatch($match){
        foreach($this->symbol_keys as $key){  // 查出实际匹配的,
            if(isset($match[$key]) && (-1 !== $match[$key][1])){
                return [$key=>$match[$key]];  //转成可用的格式
            }
        }
        return [];
    }

    /**
     * @param $match
     *
     * @return mixed
     */
    protected function addToken($match){

        $key = key($match);  // 上面转的那个格式  [$key=>$match[$key]] 所以,可以拿到key

        [$symbol,$offset] = $match[$key];  // 再取出symbol,offset
       // 通过addItem方法加到listLexer中,这里,token有一个previous_type 上一节点类型,及早及时写入,免得后续再要处理
        $this->listLexer->addItem(Token::from($key,$symbol,$this->previous_type));

        /**
         * set the next_token_type for last token
         */
        $this->listLexer->setNextType($key);   // 对上一个节点,告诉它,下一个节点的类型是什么。

        $this->previous_type = $key;  //重置previous_type 到当前的key

        return $offset + strlen($symbol);  //返回 编移,告诉FOR循环,结束了没有
    }


    /**
     * @param $rql_str
     *  我们在Parser类中调用的是它,那我们看它做了什么
     * @return \ByteFerry\RqlParser\Lexer\ListLexer
     * @throws \ByteFerry\RqlParser\Exceptions\RegexException
     */
    public function tokenise($rql_str){ 
        //首先,创建 ListLexer实例
        $this->listLexer = ListLexer::of();
        /**
         * using all the regular expressions
         */
        $math_expression = Symbols::makeExpression();  //这里,调用了这一函数,把所有正则表达式装进了数组,到此,上面说的,等用到时再讲的两个函数,这里都讲到了。

        $rql_str = trim($rql_str);   // 去空格

        $end_pos = strlen($rql_str);   // 获取长度

        for($offset=0;$offset<$end_pos;){  //循环匹配表达式,
            preg_match($math_expression, $rql_str, $result,PREG_OFFSET_CAPTURE,$offset);
            if (preg_last_error() !== PREG_NO_ERROR) {   // 如果出错,抛出异常,因为,这很重要,要对程序员友好,不要做第二个Tp.
                throw new ParseException(array_flip(get_defined_constants(true)['pcre'])[preg_last_error()]);
            }

            /**
             * get the result from matches
             */
            $match = $this->getMatch($result);  //将匹配结果进行格式转换。

            /**
             * update the offset
             */
            $offset = $this->addToken($match);  添加到本类的token数组中
        }

        if(0 !== $this->listLexer->getLevel()){   // 同样,如果括号不匹配,那也是语法错误,抛出异常。
            throw new ParseException('The bracket are not paired.');
        }

        return $this->listLexer;  // 返回listLexer
    }


}

我们可以看出,Lexer只做了一件事,就是把字符串中通过正则匹配到的转换成Token,写到listLexer中,待后续进一步操作。 (待续)

继续阅读:

用PHP从零开始写一个编译器(二)

用PHP从零开始写一个编译器(三)

用PHP从零开始写一个编译器(四)

用PHP从零开始写一个编译器(五)