hafriedlander / php-peg
PHP的PEG(解析表达式语法)编译器
Requires
- php: >=5.3.0
This package is not auto-updated.
Last update: 2024-09-14 15:05:58 UTC
README
这是一个PHP的解析表达式语法编译器。PEG解析器是其他CFG语法的替代品,它将标记化和词法分析结合在一个自上而下的语法中。有关主题的基本概述,请参阅http://en.wikipedia.org/wiki/Parsing_expression_grammar
快速入门
- 编写解析器。解析器是一个包含在特殊语法中的PHP类,其文件类型为.peg.inc。请参阅示例目录。
- 编译解析器:
php ./cli.php ExampleParser.peg.inc > ExampleParser.php
- 使用解析器(您还可以在输入解析器中包含用于此目的的代码 - 再次请参阅示例目录)
$x = new ExampleParser( 'string to parse' ) ;
$res = $x->match_Expr() ;
解析器格式
解析器包含在一个PHP文件中,在一个或多个以/*!* [name | !pragma]
(类似于文档块,但星号中间有一个感叹号)开始的特殊注释块中。
您可以有多个注释块,所有这些块在编译时都被视为连续的。在编译过程中,这些块将被替换为每个块中每个规则的“匹配”函数(匹配字符串与其规则的函数)集。
可选的名称标记着新的解析器规则集的开始。目前这个功能尚未使用,但将来可能会用于优化和调试目的。如果没有指定,则默认为上一个解析器注释块的名称,或者如果没有设置过名称,则默认为'Anonymous Parser'。
如果名称以'!'符号开头,则该注释块是一个预处理器指令(pragma),它不是解析器的一部分,而是一个特殊的数据块。
从词法上讲,这些块是一组规则和注释。一个规则可以是基本规则或扩展规则。
基本规则
基本规则由规则的名称、一些可选的参数、匹配规则本身以及一个可选的附加函数集组成。
NAME ( "(" ARGUMENT, ... ")" )? ":" MATCHING_RULE
ATTACHED_FUNCTIONS?
名称必须由字符a-z、A-Z、0-9以及_和-组成,并且不能以数字开头。
基本规则可以跨越多行,只要后续行都有缩进。
扩展规则
扩展规则要么与基本规则相同,但增加了要扩展的规则的名称,要么是一个替换扩展,由规则的名称、要扩展的规则的名称以及可选的一些参数、一些替换和一个附加的函数集组成。
NAME extend BASENAME ( "(" ARGUMENT, ... ")" )? ":" MATCHING_RULE
ATTACHED_FUNCTIONS?
NAME extends BASENAME ( "(" ARGUMENT, ... ")" )? ( ";" REPLACE "=>" REPLACE_WITH, ... )?
ATTACHED_FUNCTIONS?
技巧和陷阱
我们允许缩进解析器块,但必须以一致的方式缩进 - whatever the indent of the /*** marker becomes the "base" indent, and needs to be used for all lines. You can mix tabs and spaces, but the indent must always be an exact match - if the "base" indent is a tab then two spaces, every line within the block also needs indenting with a tab then two spaces, not two tabs (even if in your editor, that gives the same indent).
任何超过"base"缩进的行都被认为是前一个规则的延续。
任何少于"base"缩进的行都是错误。
如果我有空重新编写内部“parser parser”为php-peg,整个过程可能会更灵活。
规则
PEG 匹配规则试图遵循标准 PEG 格式,总结如下
token* - Token is optionally repeated
token+ - Token is repeated at least one
token? - Token is optionally present
tokena tokenb - Token tokenb follows tokena, both of which are present
tokena | tokenb - One of tokena or tokenb are present, preferring tokena
&token - Token is present next (but not consumed by parse)
!token - Token is not present next (but not consumed by parse)
( expression ) - Grouping for priority
但是有这些扩展
< or > - Optionally match whitespace
[ or ] - Require some whitespace
标记
标记可以是
- 裸词,它们是递归匹配器 - 对语法中其他地方定义的标记规则的引用,
- 字面量,由
"
或'
引号对包围。字面量中不提供转义支持。 - 正则表达式,由
/
对包围。 - 表达式 - 以
$
开头的单个单词(匹配 \w+)或包围在${ }
中的更复杂表达式,它调用用户定义的函数来执行匹配
正则表达式标记
自动锚定到当前字符串的开始 - 不要在任何地方包含字符串开始锚点(^
)。始终像在 PHP 中启用 'x' 标志一样操作 - 忽略空白,除非转义,并且 '#' 开始一个注释。
注意正则表达式标记的结尾 - 《em>/' 模式(如 /foo\s/)将结束 PHP 注释。由于 'x' 标志始终启用,只需用空格分割即可(如 / foo \s* /)。
表达式
表达式允许运行时计算匹配。您可以在字面量或正则表达式标记内嵌入表达式来匹配计算值,或者简单地将表达式指定为标记来匹配动态规则。
表达式栈
当获取用于表达式的值时,解析器将向上遍历栈以查找设置的值。表达式栈是通过传递到此处为止的所有规则列表。例如,给定解析器
A: $a
B: A
C: B
查找 $a 的表达式栈将是 C, B, A - 换句话说,将首先检查 A 规则,然后是 B,最后是 C。
在终端(字面量和正则表达式)中
标记将被替换为查找的值。要查找标记的值,表达式栈将向上遍历检查以下之一
- 结果数组节点中的键/值对
- 规则附加的方法包括
$
(即function $foo()
)
如果没有找到值,它将检查解析器上是否存在方法或属性(不包括 $
)。如果没有找到这些,表达式将被替换为空字符串。
作为标记
标记将被查找以找到值,该值必须是匹配规则的名字。然后,将像该规则的重入标记一样匹配该规则。
要找到要匹配的规则的名字,表达式栈将向上遍历检查以下之一
- 结果数组节点中的键/值对
- 规则附加的方法包括
$
(即function $foo()
)
如果没有找到值,它将检查解析器上是否存在方法或属性(不包括 $
)。如果没有找到这些,规则将无法匹配。
技巧和陷阱
注意不要在打算使用终端表达式时使用标记表达式,例如
quoted_good: q:/['"]/ string "$q"
quoted_bad: q:/['"]/ string $q
"$q"
再次匹配 q 的值。 $q
尝试匹配名为 "
或 '
的规则(这两个都是非法规则名,因此会失败)
命名匹配规则
可以通过在名称前添加名称和 :
来给标记和组命名,例如
rulea: "'" name:( tokena tokenb )* "'"
名称和 :
之间不能有空格
badrule: "'" name : ( tokena tokenb )* "'"
递归匹配器可以通过仅使用 :
来具有与它们的规则名相同的名称。以下两个规则是等效的
rulea: tokena tokenb:tokenb
rulea: tokena :tokenb
规则附加的函数
每个规则都可以附加一组函数。这些函数可以定义
- 在语法内部通过在规则后面缩进函数体来定义
- 在类中通过在语法注释之后关闭定义一个名为
{$rulename}_{$functionname}
或{$rulename}{$functionname}
(如果函数名以_
开头)的常规方法来定义,或者{$rulename}{$functionname}
- 在子类中
所有不在语法中的函数都必须有PHP兼容的名字(参见PHP名称映射)。语法中的函数在需要时将转换其名字。
所有这些定义都定义了同一个规则关联的函数
<?php class A extends Parser { /*!* Parser foo: bar baz function bar() {} */ function foo_bar() {} } class B extends A { function foo_bar() {} } ?>
PHP名称映射
语法中的规则映射到名为match_{$rulename}
的PHP函数。然而,规则名称可以包含PHP函数不能包含的字符。这些字符将被重新映射
'-' => '_'
'$' => 'DLR'
'*' => 'STR'
其他不允许的字符将被移除。
结果
结果是嵌套数组的树。
在没有特定控制的情况下,每个规则的输出将仅是它在['text']
成员中匹配的文本。此成员必须始终存在。
通过给子表达式、字面量、正则表达式或递归匹配命名(参见命名匹配规则),将在结果数组中插入一个以该名称命名的成员。如果只有一个匹配项,它将是一个单独的结果数组。如果有多个匹配项,它将是数组数组。
您可以通过指定具有给定名称的规则关联函数来覆盖结果存储。它将使用当前结果数组和子匹配项(在这种情况下是子匹配项)的引用来调用。在这种情况下,默认存储操作将不会发生。
如果您为递归匹配指定了一个规则关联函数,您根本不需要命名该标记 - 它将被自动调用,例如
rulea: tokena tokenb
function tokenb ( &$res, $sub ) { print 'Will be called, even though tokenb is not named or marked with a :' ; }
您还可以指定一个名为*
的规则关联函数,该函数将调用每个递归匹配
rulea: tokena tokenb
function * ( &$res, $sub ) { print 'Will be called for both tokena and tokenb' ; }
静默匹配
默认情况下,所有匹配都添加到结果的'text'属性。通过在成员前加.
,该匹配将不会添加到['text']成员。这不会影响其他结果属性,这些属性由命名规则添加。
继承
规则可以使用extends关键字从其他规则继承。有几种方法可以更改规则的匹配,但它们都有一个共同特点 - 在构建结果集时,规则还将检查继承的规则的规则关联函数以存储处理程序。这使得您可以做到像
A: Foo Bar Baz
function *(){ /* Generic store handler */ }
B extends A
function Bar(){ /* Custom handling for Bar - Foo and Baz will still fall through to the A#* function defined above */ }
实际匹配规则可以以三种方式指定
复制
如果您没有指定新规则或替换集,匹配规则将按原样复制。这在您想覆盖一些存储逻辑但不想覆盖规则本身时很有用。
文本替换
您可以使用分号代替扩展规则名称后的冒号来使用测试替换替换继承规则的一些部分。然后,您可以放置一个逗号分隔的替换列表。以下是一个示例
A: Foo | Bar | Baz
# Makes B the equivalent of Foo | Bar | (Baz | Qux)
B extends A: Baz => (Baz | Qux)
请注意,替换项没有引号。例外情况是当您想要用空字符串替换时,例如
A: Foo | Bar | Baz
# Makes B the equivalent of Foo | Bar
B extends A: | Baz => ""
目前不支持转义 - 如果您想替换","或"=>"字符,您必须使用完整替换。
完整替换
您可以使用与继承规则相同的格式指定一个全新的规则,例如
A: Foo | Bar | Baz
B extends A: Foo | Bar | (Baz Qux)
这在规则更改太多,文本替换无法阅读,但想要保留存储逻辑时很有用。
编译指示
当打开解析器注释块时,如果在名称(或没有名称)处放置以'!'开头的单词,则该注释块被视为编译指示 - 不是解析器语言本身的一部分,而是对编译器的其他指令。目前理解这些编译指示
!silent
This is a comment that should only appear in the source code. Don't output it in the generated code.
!insert_autogen_warning
Insert a warning comment into the generated code at this point, warning that the file is autogenerated and not to edit it.
待办事项
- 允许配置空白 - 指定什么匹配,以及它是否应该以原样、折叠或根本不注入到结果中
- 允许将规则内联到其他规则中以提高速度
- 更多优化
- 使Parser-parser能够自我生成,而不是像现在这样的手工制作解析器。
- PHP标记解析器和其他标记流,而不是像现在这样仅限于字符串