railt / compiler
Requires
- php: >=7.1.3
- ext-json: *
- ext-mbstring: *
- ext-pcre: *
- ext-spl: *
- railt/io: ~1.3.0|1.3.x-dev
- railt/lexer: ~1.3.0|1.3.x-dev
- railt/parser: ~1.3.5|1.3.x-dev
- zendframework/zend-code: ~3.0
Requires (Dev)
- phpunit/phpunit: ^6.5
- railt/parser: ~1.3.0|1.3.x-dev
- symfony/finder: ~4.0
README
编译器
这是基于Hoa\Compiler基本功能的所谓编译器-编译器实现。
此库用于从语法文件创建解析器,在解析过程中本身并不使用,这只在开发中需要。
在您开始使用自定义解析器实现之前,建议您查看EBNF。
语法
每种语言都由添加到句子的单词组成。为了正确构建提案,需要一些规则。这些规则被称为语法。
让我们尝试为计算器创建相应的语法,该计算器可以添加两个数字。如果您熟悉替代语法(Antlr,BNF,EBNF,Hoa等),那么这不会很难。
(* "sum" is a rule that determines the sequence of a number, an addition symbol and one more number *) sum = digit plus digit ; (* "digit" is one of the available numeric characters *) digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ; (* "plus" is a plus sign. Incredibly! *) plus = "+" ;
Railt的语法部分不同于原始的EBNF。这样,让我们将相同的规则重新结构化为Railt的语法。
// The rule "digit" can be replaced by a simple lexeme, // which can be expressed in a PCRE "\d". %token T_DIGIT \d // The same applies to the "+" token. %token T_PLUS \+ // All whitespace chars must be ignored. %skip T_WHITESPACE \s+ // Now we need to determine the "sum" rule, which will correspond // to the previous version. #Sum: <T_DIGIT> ::T_PLUS:: <T_DIGIT> ;
为了简单地测试性能,只需在语法上读取和播放即可!
use Railt\Component\Io\File; use Railt\Component\Compiler\Compiler; $parser = Compiler::load(File::fromSources(' /** * Grammar sources */ %token T_DIGIT \d %token T_PLUS \+ %skip T_WHITESPACE \s+ #Sum : <T_DIGIT> ::T_PLUS:: <T_DIGIT> ; ')); echo $parser->parse(File::fromSources('2 + 2'));
在输出中,您将得到一个AST,它将由echo
运算符序列化为XML,如下所示
<Ast> <Sum offset="0"> <T_DIGIT offset="0">2</T_DIGIT> <T_DIGIT offset="2">2</T_DIGIT> </Sum> </Ast>
命名寄存器不重要,但建议您将令牌命名为大写("TOKEN_NAME"),将规则命名为大写("RuleName")。这样的建议将帮助您在将来更容易地浏览现有的语法。
定义
在Railt语法中,有5种类型的定义
%token name regex
- 定义令牌的名称和值。%skip name regex
- 定义跳过令牌的名称和值。这样的令牌将被忽略,并且可以在语法的任何地方允许。%pragma name value
- 配置词法分析和解析器的规则。%include path/to/file
- 链接到另一个语法文件。rule
或#rule
- 语法规则。
注释
在Railt语法中,有两种类似C的注释类型
// 行注释
- 这种注释类型以两个斜杠开始,以行尾结束。/* 多行注释 */
- 这种注释类型以/*
符号开始,以*/
符号结束。
输出控制
您可能已经注意到,在语法中,令牌的定义看起来略有不同:<TOKEN>
和::TOKEN::
。
这种在语法中确定令牌的方式告诉编译器是否将顺序令牌打印为结果。这就是为什么忽略“加号”令牌的原因,因为我们不需要有关此令牌的信息,但“数字”令牌的值对我们来说很重要。
<TOKEN>
- 在AST中保留令牌。::TOKEN::
- 从AST中隐藏令牌。
声明规则
每个规则以该规则的名称开头。此外,每个规则都可以用#
符号标记,表示该规则应该保留在AST中。
#Rule
- 定义规则必须在AST中存在。Rule
- 定义规则应该从AST中隐藏。
名称之后是规则的生成(主体),它们由一个有效字符分开:=
或:
。分隔符字符不重要,并作为与其他语法的兼容性存在。此外,规则可以以可选的;
字符结束。
PP2语言的构造如下
rule()
- 调用一个规则。<token>
和::token::
- 声明一个词素。|
- 表示析取(或“交替”)。(…)
- 表示一个分组。e?
- 表示e
是可选的
(0或1次)。e+
- 表示e
可以出现1或更多
次。e*
- 表示e
可以出现0或更多
次。e{x,y}
(e{,y}
,e{x,}
或e{x}
) - 表示e
可以出现在x和y之间
次。#rule
- 在结果树中创建规则节点。
最后,PP2语言的语法是用PP2语言编写的。
让我们尝试添加计算器剩余符号的支持:除法、除法和减法;同时略微改进词法分析器的规则。
%skip T_WHITESPACE \s+ %token T_DIGIT \-?\d+ %token T_PLUS \+ %token T_MINUS \- %token T_DIV / %token T_MUL \* #Expression : Operation() ; Operation : <T_DIGIT> ( Addition() | Division() | Subtraction() | Multiplication() )? ; #Addition : ::T_PLUS:: Operation() ; #Division : ::T_DIV:: Operation() ; #Subtraction : ::T_MINUS:: Operation() ; #Multiplication : ::T_MUL:: Operation() ;
简单表达式4 + 8 - 15 * 16 / 23 + -42
将被解析成以下树
<Ast> <Expression offset="0"> <T_DIGIT offset="0">4</T_DIGIT> <Addition offset="2"> <T_DIGIT offset="4">8</T_DIGIT> <Subtraction offset="6"> <T_DIGIT offset="8">15</T_DIGIT> <Multiplication offset="11"> <T_DIGIT offset="13">16</T_DIGIT> <Division offset="16"> <T_DIGIT offset="18">23</T_DIGIT> <Addition offset="21"> <T_DIGIT offset="23">-42</T_DIGIT> </Addition> </Division> </Multiplication> </Subtraction> </Addition> </Expression> </Ast>
注意,语法相当简单,不包含运算符的优先级。
委托
您可以使用规则定义名称后的关键字->
告诉编译器包含所需语法规则的PHP类。在这种情况下,每个处理的规则将创建目标类的实例。
#Digit -> Path\To\Class : <T_DIGIT> ;
有关委托的更多信息,请参阅解析器文档。
解析器编译
读取语法是一个相当简单的操作,但仍需要时间来执行。在语法规则已经形成后,您可以将它们“固定”在单独的解析器类中,该类将包含所有逻辑,并且不再需要读取源代码。在将其编译成类之后,此包(railt/compiler)可以从composer依赖中排除。
$compiler = Compiler::load(File::fromPathname('path/to/grammar.pp2')); $compiler->setNamespace('Example') ->setClassName('Parser') ->saveTo(__DIR__);
此代码示例将在当前目录中创建一个解析器类,包含所需的类和命名空间名称。生成结果的示例可以在现有项目中找到。作为源文件,请参阅此语法文件。