最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 用PHP从零开始写一个编译器(一)

    正文概述 转载于:掘金(祁宏)   2021-05-11   511

    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从零开始写一个编译器(一)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元