C++17 仅标头 PEG(解析表达式语法)库。您只需在项目中包含peglib.h
即可立即开始使用它。
由于该库仅支持 C++17 编译器,因此请确保启用编译器选项-std=c++17
。 ( /std:c++17 /Zc:__cplusplus
对于 MSVC)
您还可以尝试在线版本 PEG Playground,网址为 https://yhirose.github.io/cpp-peglib。
Bryan Ford 的文档第 2 页对 PEG 语法进行了详细描述。 cpp-peglib目前还支持以下附加语法:
'...'i
(不区分大小写的文字运算符)
[...]i
(不区分大小写的字符类运算符)
[^...]
(否定字符类运算符)
[^...]i
(不区分大小写的否定字符类运算符)
{2,5}
(类似正则表达式的重复运算符)
<
... >
(令牌边界运算符)
~
(忽略运算符)
x20
(十六进制数字字符)
u10FFFF
(Unicode 字符)
%whitespace
(自动跳过空格)
%word
(字表达式)
$name(
... )
(捕获范围运算符)
$name<
... >
(命名捕获运算符)
$name
(反向引用运算符)
|
(字典运算符)
↑
(剪切操作符)
MACRO_NAME(
... )
(参数化规则或宏)
{ precedence L - + L / * }
(解析中缀表达式)
%recovery(
... )
(错误恢复运算符)
exp⇑label
或exp^label
( (exp / %recover(label))
的语法糖)
label { error_message "..." }
(错误信息说明)
{ no_ast_opt }
(无 AST 节点优化指令)
默认情况下将进行“输入结束”检查。要禁用检查,请调用disable_eoi_check
。
该库支持称为Packrat解析的线性时间解析。
对于某些 Linux 发行版(例如 Ubuntu 和 CentOS)的重要提示:链接时需要-pthread
选项。参见#23、#46 和#62。
我相信您会喜欢 bert hubert 撰写的这篇精彩的“使用 PEG 和 cpp-peglib 进行实用解析”文章!
这是一个简单的计算器示例。它展示了如何定义语法、将语义动作与语法关联以及处理语义值。
// (1) 包含头文件#include <peglib.h>#include <assert.h>#include <iostream>using namespace peg;using namespace std;int main(void) { // (2) 创建解析器 parser parser(R"( # 计算器语法... Additive <- 乘法 '+' 加法 / 乘法 乘法 <- Primary '*' 乘法 / Primary Primary <- '(' 加法 ')' / Number Number <- < [ 0-9]+ > %空白 <- [ t]* )"); 断言(static_cast<bool>(解析器) == true); // (3) 设置动作 parser["Additive"] = [](const SemanticValues &vs) {switch (vs.choice()) {case 0: // “乘法 '+' 加法” return any_cast<int>(vs[0]) + any_cast< int>(vs[1]);默认值: // “乘法” return any_cast<int>(vs[0]); } }; parser["Multiplicative"] = [](const SemanticValues &vs) {switch (vs.choice()) {case 0: // "Primary '*' Multiplicative" return any_cast<int>(vs[0]) * any_cast< int>(vs[1]);default: // “Primary” return any_cast<int>(vs[0]); } }; parser["Number"] = [](const SemanticValues &vs) {return vs.token_to_number<int>(); }; // (4) 解析 parser.enable_packrat_parsing(); // 启用 Packrat 解析。 整数值; parser.parse(" (1 + 2) * 3 ", val); 断言(val == 9); }
要显示语法文本中的语法错误:
auto Grammar = R"( # 计算器语法... Additive <- 乘法 '+' Additive / Multiplicative Multiplicative <- Primary '*' Multiplicative / Primary Primary <- '(' Additive ')' / Number Number <- < [ 0-9]+ > %空白 <- [ t]*)"; 解析器 解析器; parser.set_logger([](size_t line, size_t col, const string& msg, const string &rule) { cerr << 行 << ":" << col << ":" << msg << "n"; });自动确定 = parser.load_grammar(语法);断言(确定);
有四种可用的语义操作:
[](const SemanticValues& vs, any& dt) [](const SemanticValues& vs) [](SemanticValues& vs, any& dt) [](语义值& vs)
SemanticValues
值包含以下信息:
语义值
匹配的字符串信息
令牌信息(如果规则是文字规则或使用令牌边界运算符)
当规则为“优先选择”时的选择编号
any& dt
是一个“读写”上下文数据,可用于任何目的。初始上下文数据在peg::parser::parse
方法中设置。
语义操作可以返回任意数据类型的值,该值将由peg::any
包装。如果用户在语义操作中不返回任何内容,则将返回const SemanticValues& vs
参数中的第一个语义值。 (Yacc 解析器具有相同的行为。)
这里显示了SemanticValues
结构:
struct SemanticValues :受保护的 std::vector<any> { // 输入文本 const char* 路径; 常量字符* ss; // 匹配的字符串 std::string_view sv() const { 返回 sv_; } // 匹配字符串所在的行号和列 std::pair<size_t, size_t> line_info() const; // 代币 std::vector<std::string_view> 标记; std::string_view 标记(size_t id = 0) const; // 令牌转换 std::string token_to_string(size_t id = 0) const; template <typename T> T token_to_number() const; // 选择编号(基于 0 的索引) size_t choice() const; // 将语义值向量转换为另一个向量 模板 <类型名称 T> 矢量 <T> 变换(size_t beg = 0, size_t end = -1) const; }
以下示例使用<
... >
运算符,它是标记边界运算符。
peg::parser 解析器(R"( ROOT <- _ TOKEN (',' _ TOKEN)* TOKEN <- < [a-z0-9]+ > _ _ <- [ trn]*)"); parser["TOKEN"] = [](const SemanticValues& vs) { // 'token' 不包含尾随空格 自动令牌 = vs.token(); };auto ret = parser.parse(" token1, token2 ");
我们可以使用~
运算符从列表中忽略不必要的语义值。
peg::parser 解析器(R"( ROOT <- _ ITEM (',' _ ITEM _)* ITEM <- ([a-z0-9])+ ~_ <- [ t]*)"); parser["ROOT"] = [&](const SemanticValues& vs) { 断言(vs.size() == 2); // 应该是 2 而不是 5。};auto ret = parser.parse(" item1, item2 ");
下面的语法与上面的相同。
peg::parser 解析器(R"( ROOT <- ~_ ITEM (',' ~_ ITEM ~_)* ITEM <- ([a-z0-9])+ _ <- [ t]*)");
语义谓词支持可通过谓词操作获得。
peg::parser 解析器("NUMBER <- [0-9]+"); parser["NUMBER"] = [](const SemanticValues &vs) { return vs.token_to_number<long>(); }; parser["NUMBER"].predicate = [](const SemanticValues &vs,const std::any & /*dt*/, std::string &msg) { if (vs.token_to_number<long>() != 100) { msg = "值错误!!";返回 false; 返回真; };long val;auto ret = parser.parse("100", val);assert(ret == true);assert(val == 100); ret = parser.parse("200", val);assert(ret == false);
还可以执行进入和离开操作。
parser["RULE"].enter = [](const Context &c, const char* s, size_t n, any& dt) { std::cout <<“输入”<< std::endl; }; 解析器["RULE"] = [](const SemanticValues& vs, any& dt) { std::cout <<“行动!” << std::endl; }; parser["RULE"].leave = [](const Context &c, const char* s, size_t n, size_t matchlen, any& value, any& dt) { std::cout << "离开" << std::endl; };
您可以通过记录器接收错误信息:
parser.set_logger([](size_t line, size_t col, const string& msg) { ... }); parser.set_logger([](size_t line, size_t col, const string& msg, const string &rule) { ... });
正如您在第一个示例中看到的,我们可以使用%whitespace
规则自动忽略标记之间的空格。
%whitespace
规则可以应用于以下三种情况:
标记上的尾随空格
文本上的前导空格
规则中文字字符串的尾随空格
这些是有效的令牌:
KEYWORD <- 'keyword' KEYWORDI <- 'case_insensitive_keyword' WORD <- < [a-zA-Z0-9] [a-zA-Z0-9-_]* > # token boundary operator is used. IDNET <- < IDENT_START_CHAR IDENT_CHAR* > # token boundary operator is used.
以下语法接受one, "two three", four
。
ROOT <- ITEM (',' ITEM)* ITEM <- WORD / PHRASE WORD <- < [a-z]+ > PHRASE <- < '"' (!'"' .)* '"' > %whitespace <- [ trn]*
peg::parser 解析器(R"( ROOT <- 'hello' 'world' %whitespace <- [ trn]* %word <- [az]+)"); parser.parse("你好世界"); // OKparser.parse("helloworld"); // 错误
peg::parser 解析器(R"( ROOT <- CONTENT CONTENT <- (ELEMENT / TEXT)* ELEMENT <- $(STAG CONTENT ETAG) STAG <- '<' $tag< TAG_NAME > '>' ETAG <- '< /' $tag '>' TAG_NAME <- 'b' / 'u' TEXT <- TEXT_DATA TEXT_DATA <- ![<] .)"); parser.parse("这是<b>一个<u>测试</u>文本</b>。"); // OKparser.parse("这是<b>一个<u>测试</b>文本</u>。"); // NGparser.parse("这是<b><u>测试文本</b>。"); // 错误
|
运算符允许我们通过内部使用Trie结构来制作一个用于快速查找的单词词典。我们不必担心单词的顺序。
START <- 'This month is ' MONTH '.'
MONTH <- 'Jan' | 'January' | 'Feb' | 'February' | '...'
我们能够找到与choice()
匹配的项目。
parser["MONTH"] = [](const SemanticValues &vs) { auto id = vs.choice(); };
它支持不区分大小写的模式。
START <- 'This month is ' MONTH '.'
MONTH <- 'Jan'i | 'January'i | 'Feb'i | 'February'i | '...'i
↑
运算符可以缓解回溯性能问题,但有改变语法含义的风险。
S <- '(' ↑ P ')' / '"' ↑ P '"' / P
P <- 'a' / 'b' / 'c'
当我们用上面的语法解析(z
时,在(
匹配后,我们不必在S
中回溯,因为那里插入了一个剪切运算符。
# Syntax
Start ← _ Expr
Expr ← Sum
Sum ← List(Product, SumOpe)
Product ← List(Value, ProOpe)
Value ← Number / T('(') Expr T(')')
# Token
SumOpe ← T('+' / '-')
ProOpe ← T('*' / '/')
Number ← T([0-9]+)
~_ ← [ trn]*
# Macro
List(I, D) ← I (D I)*
T(x) ← < x > _
关于优先级攀爬算法,请看这篇文章。
解析器 解析器(R"( EXPRESSION <- IFIX_EXPRESSION(ATOM, OPERATOR) ATOM <- NUMBER / '(' EXPRESSION ')' OPERATOR <- < [-+/*] > NUMBER <- < '-'? [0-9 ]+ > %whitespace <- [ t]* # 声明优先顺序 IFIX_EXPRESSION(A, O) <- A (O A)* { 优先级 L + - L * / })"); parser["INFIX_EXPRESSION"] = [](const SemanticValues& vs) -> long { auto result = any_cast<long>(vs[0]); if (vs.size() > 1) {auto ope = any_cast<char>(vs[1]);auto num = any_cast<long>(vs[2]);switch (ope) { case '+': 结果+= 数字;休息; case '-': 结果 -= num;休息; case '*': 结果 *= num;休息; case '/': 结果 /= num;休息; } 返回结果; }; parser["OPERATOR"] = [](const SemanticValues& vs) { return *vs.sv(); }; parser["NUMBER"] = [](const SemanticValues& vs) { return vs.token_to_number<long>(); };长值; parser.parse(" -1 + (1 + 2) * 3 - -1", val);assert(val == 9);
优先指令只能应用于以下“列表”样式规则。
Rule <- Atom (Operator Atom)* { precedence L - + L / * R ^ }
优先指令包含优先信息条目。每个条目都以关联性“L”(左)或“R”(右)开头,然后是运算符文字标记。第一个条目具有最高的订单级别。
cpp-peglib能够在解析时生成 AST(抽象语法树)。 peg::parser
类上的enable_ast
方法启用该功能。
注意:AST 节点将相应的令牌保存为std::string_vew
以提高性能并减少内存使用。用户有责任保留原始源文本以及生成的 AST 树。
peg::parser parser(R"( ... definition1 <- ... { no_ast_opt } definition2 <- ... { no_ast_opt } ... )"); parser.enable_ast(); shared_ptr<peg::Ast> ast; if (parser.parse("...", ast)) { cout << peg::ast_to_s(ast); ast = parser.optimize_ast(ast); cout << peg::ast_to_s(ast); }
optimize_ast
删除冗余节点以使 AST 更简单。如果您想从特定规则中禁用此行为,可以使用no_ast_opt
指令。
它在内部调用peg::AstOptimizer
来完成这项工作。您可以创建自己的 AST 优化器来满足您的需求。
请参阅 AST 计算器示例和 PL/0 语言示例中的实际用法。
我们也可以使用解析器组合器手动构建解析器,而不是通过解析 PEG 语法文本来制作解析器。这是一个例子:
使用命名空间 peg;使用命名空间 std;向量<string> 标签; 定义ROOT、TAG_NAME、_; ROOT <= seq(_, zom(seq(chr('['), TAG_NAME, chr(']'), _))); TAG_NAME <= oom(seq(npd(chr(']')), dot())), [&](const SemanticValues& vs) { Tags.push_back(vs.token_to_string()); }; _ <= zom(cls(" t"));auto ret = ROOT.parse(" [tag1] [tag:2] [tag-3] ");
以下是可用的运算符:
操作员 | 描述 | 操作员 | 描述 |
---|---|---|---|
序列 | 顺序 | 町 | 优先选择 |
佐姆 | 零个或更多 | 奥姆 | 一个或多个 |
选择 | 选修的 | 阿帕德 | 和谓词 |
NPD | 不是谓词 | 点亮 | 文字字符串 |
利蒂 | 不区分大小写 文字字符串 | CLS | 字符类 |
NCLS | 否定字符类 | chr | 特点 |
点 | 任意角色 | 托克 | 代币边界 |
伊恩 | 忽略语义值 | CSC | 捕捉范围 |
帽 | 捕获 | 贝克尔 | 反向参考 |
迪克 | 字典 | 前 | 中缀表达式 |
记录 | 中缀表达式 | 使用者 | 用户定义的解析器 |
代表 | 重复 |
可以添加/覆盖定义。
自动语法 = R"( ROOT <- _ 'Hello' _ NAME '!' _)"; 规则 附加规则 = { {"NAME", usr([](const char* s, size_t n, SemanticValues& vs, any& dt) -> size_t { 静态向量<string> 名称 = { "PEG", "BNF" }; for (const auto& name : 名称) {if (name.size() <= n && !name.compare(0, name.size(), s, name.size())) { return name.size(); // 处理后的长度} 返回-1; // 解析错误}) }, {“~_”,zom(cls(“trn”)) } };auto g = 解析器(语法,additional_rules);assert(g.parse(“你好BNF!”));
cpp-peglib 接受 UTF8 文本。 .
匹配 Unicode 代码点。还有,支持u????
。
cpp-peglib 支持最远故障错误位置报告,如 Bryan Ford 原始文档中所述。
为了更好的错误报告和恢复,cpp-peglib 支持带有标签的“恢复”运算符,该标签可以与恢复表达式和自定义错误消息关联。这个想法来自 Sergio Medeiros 和 Fabio Mascarenhas 撰写的精彩论文“解析表达式语法中的语法错误恢复”。
自定义消息支持%t
(表示意外标记的占位符)和%c
(表示意外 Unicode 字符)。
下面是一个类似 Java 语法的例子:
# java.peg
Prog ← 'public' 'class' NAME '{' 'public' 'static' 'void' 'main' '(' 'String' '[' ']' NAME ')' BlockStmt '}'
BlockStmt ← '{' (!'}' Stmt^stmtb)* '}' # Annotated with `stmtb`
Stmt ← IfStmt / WhileStmt / PrintStmt / DecStmt / AssignStmt / BlockStmt
IfStmt ← 'if' '(' Exp ')' Stmt ('else' Stmt)?
WhileStmt ← 'while' '(' Exp^condw ')' Stmt # Annotated with `condw`
DecStmt ← 'int' NAME ('=' Exp)? ';'
AssignStmt ← NAME '=' Exp ';'^semia # Annotated with `semi`
PrintStmt ← 'System.out.println' '(' Exp ')' ';'
Exp ← RelExp ('==' RelExp)*
RelExp ← AddExp ('<' AddExp)*
AddExp ← MulExp (('+' / '-') MulExp)*
MulExp ← AtomExp (('*' / '/') AtomExp)*
AtomExp ← '(' Exp ')' / NUMBER / NAME
NUMBER ← < [0-9]+ >
NAME ← < [a-zA-Z_][a-zA-Z_0-9]* >
%whitespace ← [ tn]*
%word ← NAME
# Recovery operator labels
semia ← '' { error_message "missing semicolon in assignment." }
stmtb ← (!(Stmt / 'else' / '}') .)* { error_message "invalid statement" }
condw ← &'==' ('==' RelExp)* / &'<' ('<' AddExp)* / (!')' .)*
例如, ';'^semi
是(';' / %recovery(semi))
的语法糖。 %recover
运算符尝试恢复“;”处的错误通过使用恢复表达式semi
跳过输入文本。 semi
还与自定义消息“赋值中缺少分号”相关联。
结果如下:
> cat Sample.javapublic class 示例 { public static void main(String[] args) {int n = 5;int f = 1;while( < n) { f = f * n; } n = n - 1};System.out.println(f); } } > peglint java.peg sample.javasample.java:5:12: 语法错误,意外的 '<', 期望 '(', <NUMBER>, <NAME>.sample.java:8:5: 赋值.sample 中缺少分号.java:8:6:无效语句
正如您所看到的,它现在可以显示多个错误,并提供比默认消息更有意义的错误消息。
我们可以将自定义错误消息与定义相关联。
# custom_message.peg
START <- CODE (',' CODE)*
CODE <- < '0x' [a-fA-F0-9]+ > { error_message 'code format error...' }
%whitespace <- [ t]*
> cat custom_message.txt 0x1234,0x@@@@,0xABCD > peglint custom_message.peg custom_message.txt custom_message.txt:1:8: code format error...
注意:如果优先选择中有多个带有错误消息指令的元素,则此功能可能无法按您的预期工作。
我们可以如下更改开始定义规则。
自动语法 = R"( Start <- A A <- B (',' B)* B <- '[一]' / '[二]' %whitespace <- [ tn]*)"; peg::parser 解析器(语法,“A”); // 开始规则是“A” orpeg::parser 解析器; parser.load_grammar(语法, "A"); // 起始规则为 "A"parser.parse(" [one] , [two] "); // 好的
> cd lint > mkdir build > cd build > cmake .. > make > ./peglint usage: grammar_file_path [source_file_path] options: --source: source text --packrat: enable packrat memoise --ast: show AST tree --opt, --opt-all: optimize all AST nodes except nodes selected with `no_ast_opt` instruction --opt-only: optimize only AST nodes selected with `no_ast_opt` instruction --trace: show concise trace messages --profile: show profile report --verbose: verbose output for trace and profile
> cat a.peg Additive <- Multiplicative '+' Additive / Multiplicative Multiplicative <- Primary '*' Multiplicative / Primary Primary <- '(' Additive ')' / Number %whitespace <- [ trn]* > peglint a.peg [commandline]:3:35: 'Number' is not defined.
> cat a.peg Additive <- Multiplicative '+' Additive / Multiplicative Multiplicative <- Primary '*' Multiplicative / Primary Primary <- '(' Additive ')' / Number Number <- < [0-9]+ > %whitespace <- [ trn]* > peglint --source "1 + a * 3" a.peg [commandline]:1:3: syntax error
> cat a.txt 1 + 2 * 3 > peglint --ast a.peg a.txt + Additive + Multiplicative + Primary - Number (1) + Additive + Multiplicative + Primary - Number (2) + Multiplicative + Primary - Number (3)
> peglint --ast --opt --source "1 + 2 * 3" a.peg + Additive - Multiplicative[Number] (1) + Additive[Multiplicative] - Primary[Number] (2) - Multiplicative[Number] (3)
no_ast_opt
指令调整 AST 优化> cat a.peg Additive <- Multiplicative '+' Additive / Multiplicative Multiplicative <- Primary '*' Multiplicative / Primary Primary <- '(' Additive ')' / Number { no_ast_opt } Number <- < [0-9]+ > %whitespace <- [ trn]* > peglint --ast --opt --source "1 + 2 * 3" a.peg + Additive/0 + Multiplicative/1[Primary] - Number (1) + Additive/1[Multiplicative] + Primary/1 - Number (2) + Multiplicative/1[Primary] - Number (3) > peglint --ast --opt-only --source "1 + 2 * 3" a.peg + Additive/0 + Multiplicative/1 - Primary/1[Number] (1) + Additive/1 + Multiplicative/0 - Primary/1[Number] (2) + Multiplicative/1 - Primary/1[Number] (3)
计算器
计算器(带有解析器运算符)
计算器(AST版)
计算器(按优先级攀爬解析表达式)
计算器(AST 版本和按优先级攀爬解析表达式)
一个微型 PL/0 JIT 编译器,少于 900 个 LOC,带有 LLVM 和 PEG 解析器
一种专门用于编写 Fizz Buzz 程序的编程语言。 :)
麻省理工学院许可证(© 2022 Yuji Hirose)